diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index 949cb0a0ff534..b16e75abdb8a1 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -15,6 +15,7 @@ const STORYBOOKS = [ 'apm', 'canvas', 'cases', + 'cell_actions', 'ci_composite', 'cloud_chat', 'coloring', @@ -34,6 +35,7 @@ const STORYBOOKS = [ 'expression_shape', 'expression_tagcloud', 'fleet', + 'grouping', 'home', 'infra', 'kibana_react', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e6cd6ff041cd0..63cf2327516fa 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -455,6 +455,7 @@ src/plugins/maps_ems @elastic/kibana-gis x-pack/plugins/maps @elastic/kibana-gis x-pack/packages/ml/agg_utils @elastic/ml-ui x-pack/packages/ml/date_picker @elastic/ml-ui +x-pack/packages/ml/error_utils @elastic/ml-ui x-pack/packages/ml/is_defined @elastic/ml-ui x-pack/packages/ml/is_populated_object @elastic/ml-ui x-pack/packages/ml/local_storage @elastic/ml-ui @@ -683,6 +684,7 @@ src/plugins/unified_search @elastic/kibana-visualizations x-pack/plugins/upgrade_assistant @elastic/platform-deployment-management x-pack/plugins/drilldowns/url_drilldown @elastic/kibana-app-services src/plugins/url_forwarding @elastic/kibana-visualizations +packages/kbn-url-state @elastic/security-threat-hunting-investigations src/plugins/usage_collection @elastic/kibana-core test/plugin_functional/plugins/usage_collection @elastic/kibana-core packages/kbn-user-profile-components @elastic/kibana-security diff --git a/api_docs/actions.mdx b/api_docs/actions.mdx index 6bfdc25b53149..9f6783794ad2c 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: 2023-04-21 +date: 2023-04-24 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 4cf73a72168eb..f41313a90546b 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: 2023-04-21 +date: 2023-04-24 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 c0fa0ef3e4c02..50cb3ba36668e 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: 2023-04-21 +date: 2023-04-24 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 cfec38128269c..264f3c10d43f3 100644 --- a/api_docs/alerting.devdocs.json +++ b/api_docs/alerting.devdocs.json @@ -5261,12 +5261,20 @@ { "parentPluginId": "alerting", "id": "def-common.AlertsFilter.query", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "query", "description": [], "signature": [ - "{ kql: string; dsl?: string | undefined; } | null" + "{ kql: string; filters: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]; dsl?: string | undefined; } | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -5275,7 +5283,7 @@ { "parentPluginId": "alerting", "id": "def-common.AlertsFilter.timeframe", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "timeframe", "description": [], @@ -5287,7 +5295,7 @@ "section": "def-common.AlertsFilterTimeframe", "text": "AlertsFilterTimeframe" }, - " | null" + " | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -5541,6 +5549,20 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertStatus.maintenanceWindowIds", + "type": "Array", + "tags": [], + "label": "maintenanceWindowIds", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -5755,6 +5777,17 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.AlertSummary.revision", + "type": "number", + "tags": [], + "label": "revision", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -6322,6 +6355,20 @@ "path": "x-pack/plugins/alerting/common/execution_log_types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "alerting", + "id": "def-common.IExecutionLog.maintenance_window_ids", + "type": "Array", + "tags": [], + "label": "maintenance_window_ids", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/alerting/common/execution_log_types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -8864,12 +8911,20 @@ { "parentPluginId": "alerting", "id": "def-common.SanitizedAlertsFilter.query", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "query", "description": [], "signature": [ - "{ kql: string; } | null" + "{ kql: string; filters: ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.Filter", + "text": "Filter" + }, + "[]; } | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, @@ -8878,7 +8933,7 @@ { "parentPluginId": "alerting", "id": "def-common.SanitizedAlertsFilter.timeframe", - "type": "CompoundType", + "type": "Object", "tags": [], "label": "timeframe", "description": [], @@ -8890,7 +8945,7 @@ "section": "def-common.AlertsFilterTimeframe", "text": "AlertsFilterTimeframe" }, - " | null" + " | undefined" ], "path": "x-pack/plugins/alerting/common/rule.ts", "deprecated": false, diff --git a/api_docs/alerting.mdx b/api_docs/alerting.mdx index ad9cd7c82bac4..05cd2479d10f4 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'alerting'] --- import alertingObj from './alerting.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 603 | 1 | 582 | 42 | +| 606 | 1 | 585 | 42 | ## Client diff --git a/api_docs/apm.devdocs.json b/api_docs/apm.devdocs.json index 93eeed1f13395..f3585cfd924b4 100644 --- a/api_docs/apm.devdocs.json +++ b/api_docs/apm.devdocs.json @@ -4193,6 +4193,8 @@ "AggregationType", ".P99>]>; serviceName: ", "StringC", + "; errorGroupingKey: ", + "StringC", "; transactionType: ", "StringC", "; transactionName: ", @@ -4269,6 +4271,8 @@ "AggregationType", ".P99>]>; serviceName: ", "StringC", + "; errorGroupingKey: ", + "StringC", "; transactionType: ", "StringC", "; transactionName: ", @@ -4343,6 +4347,8 @@ "AggregationType", ".P99>]>; serviceName: ", "StringC", + "; errorGroupingKey: ", + "StringC", "; transactionType: ", "StringC", "; transactionName: ", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index a5226b9ec1a9f..39984c4c85e92 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'apm'] --- import apmObj from './apm.devdocs.json'; diff --git a/api_docs/asset_manager.mdx b/api_docs/asset_manager.mdx index 5704f9f7b9f81..3fdc2e696e3f0 100644 --- a/api_docs/asset_manager.mdx +++ b/api_docs/asset_manager.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/assetManager title: "assetManager" image: https://source.unsplash.com/400x175/?github description: API docs for the assetManager plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'assetManager'] --- import assetManagerObj from './asset_manager.devdocs.json'; diff --git a/api_docs/banners.mdx b/api_docs/banners.mdx index 5fcf3933483c9..f79d12ad047ef 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: 2023-04-21 +date: 2023-04-24 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 d3a45d4595553..fc6dbe517b369 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: 2023-04-21 +date: 2023-04-24 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 edb1a65ab929d..bbeae3846322f 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: 2023-04-21 +date: 2023-04-24 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 48d5f0ea25d03..15ec086b8eaf2 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: 2023-04-21 +date: 2023-04-24 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 e4c1c729146d8..8a55c2821385f 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: 2023-04-21 +date: 2023-04-24 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 0ef73b05d8bb0..c09cbda53a15c 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloud'] --- import cloudObj from './cloud.devdocs.json'; diff --git a/api_docs/cloud_chat.mdx b/api_docs/cloud_chat.mdx index 28bf235d4e803..53a1a634088eb 100644 --- a/api_docs/cloud_chat.mdx +++ b/api_docs/cloud_chat.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudChat title: "cloudChat" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudChat plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudChat'] --- import cloudChatObj from './cloud_chat.devdocs.json'; diff --git a/api_docs/cloud_data_migration.mdx b/api_docs/cloud_data_migration.mdx index ea00c024dabf2..cf1dca265856b 100644 --- a/api_docs/cloud_data_migration.mdx +++ b/api_docs/cloud_data_migration.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDataMigration title: "cloudDataMigration" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDataMigration plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDataMigration'] --- import cloudDataMigrationObj from './cloud_data_migration.devdocs.json'; diff --git a/api_docs/cloud_defend.mdx b/api_docs/cloud_defend.mdx index b6680d6d7dbd1..815fae9c2fb42 100644 --- a/api_docs/cloud_defend.mdx +++ b/api_docs/cloud_defend.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudDefend title: "cloudDefend" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudDefend plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudDefend'] --- import cloudDefendObj from './cloud_defend.devdocs.json'; diff --git a/api_docs/cloud_experiments.mdx b/api_docs/cloud_experiments.mdx index 08bce01d73062..c56ad03988470 100644 --- a/api_docs/cloud_experiments.mdx +++ b/api_docs/cloud_experiments.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/cloudExperiments title: "cloudExperiments" image: https://source.unsplash.com/400x175/?github description: API docs for the cloudExperiments plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'cloudExperiments'] --- import cloudExperimentsObj from './cloud_experiments.devdocs.json'; diff --git a/api_docs/cloud_security_posture.mdx b/api_docs/cloud_security_posture.mdx index a88a3ac25809b..7b95ed904f226 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: 2023-04-21 +date: 2023-04-24 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 aaa30afdee1ef..2f84a61fdae2e 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'console'] --- import consoleObj from './console.devdocs.json'; diff --git a/api_docs/content_management.devdocs.json b/api_docs/content_management.devdocs.json index b1fc3a2404541..7a800426b50ad 100644 --- a/api_docs/content_management.devdocs.json +++ b/api_docs/content_management.devdocs.json @@ -1259,7 +1259,7 @@ "section": "def-server.ContentStorage", "text": "ContentStorage" }, - "" + "" ], "path": "src/plugins/content_management/server/core/types.ts", "deprecated": false, @@ -1773,15 +1773,147 @@ { "parentPluginId": "contentManagement", "id": "def-server.ContentStorage.mSearch", - "type": "Object", + "type": "Uncategorized", "tags": [], "label": "mSearch", "description": [ "\nOpt-in to multi-type search.\nCan only be supported if the content type is backed by a saved object since `mSearch` is using the `savedObjects.find` API." ], "signature": [ - "MSearchConfig", - " | undefined" + "TMSearchConfig | undefined" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig", + "type": "Interface", + "tags": [], + "label": "MSearchConfig", + "description": [ + "\nA configuration for multi-type search.\nBy configuring a content type with a `MSearchConfig`, it can be searched in the multi-type search.\nUnderneath content management is using the `savedObjects.find` API to search the saved objects." + ], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "server", + "docId": "kibContentManagementPluginApi", + "section": "def-server.MSearchConfig", + "text": "MSearchConfig" + }, + "" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.savedObjectType", + "type": "string", + "tags": [], + "label": "savedObjectType", + "description": [ + "\nThe saved object type that corresponds to this content type." + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.toItemResult", + "type": "Function", + "tags": [], + "label": "toItemResult", + "description": [ + "\nMapper function that transforms the saved object into the content item result." + ], + "signature": [ + "(ctx: ", + { + "pluginId": "contentManagement", + "scope": "server", + "docId": "kibContentManagementPluginApi", + "section": "def-server.StorageContext", + "text": "StorageContext" + }, + ", savedObject: ", + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindResult", + "text": "SavedObjectsFindResult" + }, + ") => T" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.toItemResult.$1", + "type": "Object", + "tags": [], + "label": "ctx", + "description": [], + "signature": [ + { + "pluginId": "contentManagement", + "scope": "server", + "docId": "kibContentManagementPluginApi", + "section": "def-server.StorageContext", + "text": "StorageContext" + } + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.toItemResult.$2", + "type": "Object", + "tags": [], + "label": "savedObject", + "description": [], + "signature": [ + { + "pluginId": "@kbn/core-saved-objects-api-server", + "scope": "common", + "docId": "kibKbnCoreSavedObjectsApiServerPluginApi", + "section": "def-common.SavedObjectsFindResult", + "text": "SavedObjectsFindResult" + }, + "" + ], + "path": "src/plugins/content_management/server/core/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "contentManagement", + "id": "def-server.MSearchConfig.additionalSearchFields", + "type": "Array", + "tags": [], + "label": "additionalSearchFields", + "description": [ + "\nAdditional fields to search on. These fields will be added to the search query.\nBy default, only `title` and `description` are searched." + ], + "signature": [ + "string[] | undefined" ], "path": "src/plugins/content_management/server/core/types.ts", "deprecated": false, diff --git a/api_docs/content_management.mdx b/api_docs/content_management.mdx index 1f578689d0a00..21f71777a11e1 100644 --- a/api_docs/content_management.mdx +++ b/api_docs/content_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/contentManagement title: "contentManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the contentManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'contentManagement'] --- import contentManagementObj from './content_management.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 143 | 0 | 124 | 7 | +| 149 | 0 | 126 | 6 | ## Client diff --git a/api_docs/controls.mdx b/api_docs/controls.mdx index 9e673b08f099a..c280197a6417a 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'controls'] --- import controlsObj from './controls.devdocs.json'; diff --git a/api_docs/custom_integrations.mdx b/api_docs/custom_integrations.mdx index 5f75f99f25e24..53316962496a5 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: 2023-04-21 +date: 2023-04-24 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 eee68e6d1bd7b..ee4d46dc9145f 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: 2023-04-21 +date: 2023-04-24 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 707e03a38207b..e79d7c3abd944 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: 2023-04-21 +date: 2023-04-24 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 f22998f41aaf7..f58d4af3b6e61 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: 2023-04-21 +date: 2023-04-24 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 d08cdb5429fa8..8fd1bc5eca79f 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: 2023-04-21 +date: 2023-04-24 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 810eacf23336f..f56c297a354ba 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: 2023-04-21 +date: 2023-04-24 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 9c8b060ef7919..776cf231e1e62 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: 2023-04-21 +date: 2023-04-24 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 c5097f4dc01c1..4ac05486048f3 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: 2023-04-21 +date: 2023-04-24 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 ddb9680520f03..1e27fc11d34a9 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: 2023-04-21 +date: 2023-04-24 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 9301c42220b3a..57c1558717fa7 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: 2023-04-21 +date: 2023-04-24 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 aca7cffe95904..1a34b399f959c 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: 2023-04-21 +date: 2023-04-24 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 f730084172123..b98c2fea8fc27 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index cacf63a726fb4..b49d0197cbea0 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/deprecations_by_team.mdx b/api_docs/deprecations_by_team.mdx index c210b2af4a6b3..1a29e749e4dc7 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana'] --- diff --git a/api_docs/dev_tools.mdx b/api_docs/dev_tools.mdx index 880a19dbd0e91..73de9827ebc22 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: 2023-04-21 +date: 2023-04-24 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 f6cb112f22cc5..cde28fb9b20e0 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: 2023-04-21 +date: 2023-04-24 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 b3793bce178b0..79aea49ce0239 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'discoverEnhanced'] --- import discoverEnhancedObj from './discover_enhanced.devdocs.json'; diff --git a/api_docs/ecs_data_quality_dashboard.mdx b/api_docs/ecs_data_quality_dashboard.mdx index fb9dfb81dacae..ec0192478b19a 100644 --- a/api_docs/ecs_data_quality_dashboard.mdx +++ b/api_docs/ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/ecsDataQualityDashboard title: "ecsDataQualityDashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the ecsDataQualityDashboard plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ecsDataQualityDashboard'] --- import ecsDataQualityDashboardObj from './ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/embeddable.mdx b/api_docs/embeddable.mdx index 1dca0ef9fb7d2..1de1e6f809d6b 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: 2023-04-21 +date: 2023-04-24 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 5826149eca609..24c39169fe91b 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: 2023-04-21 +date: 2023-04-24 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 bc44d093b0ff4..6004d2b66f179 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: 2023-04-21 +date: 2023-04-24 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 9ad8338b79886..68f4430bbeaae 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: 2023-04-21 +date: 2023-04-24 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 7b08e077f39e1..9e32f4ae6e75b 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: 2023-04-21 +date: 2023-04-24 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 7d9413f9555b1..77ff102f16bcb 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: 2023-04-21 +date: 2023-04-24 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 c4bf518f79333..b3075cced41d8 100644 --- a/api_docs/event_log.devdocs.json +++ b/api_docs/event_log.devdocs.json @@ -1514,7 +1514,7 @@ "label": "data", "description": [], "signature": [ - "(Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" + "(Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" ], "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", "deprecated": false, @@ -1534,7 +1534,7 @@ "label": "IEvent", "description": [], "signature": [ - "DeepPartial | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, @@ -1549,7 +1549,7 @@ "label": "IValidatedEvent", "description": [], "signature": [ - "Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined" + "Readonly<{ log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; message?: string | undefined; tags?: string[] | undefined; rule?: Readonly<{ id?: string | undefined; name?: string | undefined; description?: string | undefined; category?: string | undefined; uuid?: string | undefined; version?: string | undefined; license?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; } & {}> | undefined; kibana?: Readonly<{ action?: Readonly<{ id?: string | undefined; name?: string | undefined; execution?: Readonly<{ source?: string | undefined; uuid?: string | undefined; } & {}> | undefined; } & {}> | undefined; alerting?: Readonly<{ outcome?: string | undefined; summary?: Readonly<{ recovered?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; new?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; ongoing?: Readonly<{ count?: string | number | undefined; } & {}> | undefined; } & {}> | undefined; status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; alert?: Readonly<{ rule?: Readonly<{ consumer?: string | undefined; revision?: string | number | undefined; execution?: Readonly<{ uuid?: string | undefined; metrics?: Readonly<{ number_of_triggered_actions?: string | number | undefined; number_of_generated_actions?: string | number | undefined; alert_counts?: Readonly<{ recovered?: string | number | undefined; active?: string | number | undefined; new?: string | number | undefined; } & {}> | undefined; number_of_searches?: string | number | undefined; total_indexing_duration_ms?: string | number | undefined; es_search_duration_ms?: string | number | undefined; total_search_duration_ms?: string | number | undefined; execution_gap_duration_s?: string | number | undefined; rule_type_run_duration_ms?: string | number | undefined; process_alerts_duration_ms?: string | number | undefined; trigger_actions_duration_ms?: string | number | undefined; process_rule_duration_ms?: string | number | undefined; claim_to_start_duration_ms?: string | number | undefined; prepare_rule_duration_ms?: string | number | undefined; total_run_duration_ms?: string | number | undefined; total_enrichment_duration_ms?: string | number | undefined; } & {}> | undefined; status?: string | undefined; status_order?: string | number | undefined; } & {}> | undefined; rule_type_id?: string | undefined; } & {}> | undefined; uuid?: string | undefined; flapping?: boolean | undefined; maintenance_window_ids?: string[] | undefined; } & {}> | undefined; version?: string | undefined; server_uuid?: string | undefined; task?: Readonly<{ id?: string | undefined; schedule_delay?: string | number | undefined; scheduled?: string | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; namespace?: string | undefined; rel?: string | undefined; type_id?: string | undefined; space_agnostic?: boolean | undefined; } & {}>[] | undefined; space_ids?: string[] | undefined; } & {}> | undefined; event?: Readonly<{ type?: string[] | undefined; reason?: string | undefined; action?: string | undefined; id?: string | undefined; start?: string | undefined; end?: string | undefined; category?: string[] | undefined; outcome?: string | undefined; code?: string | undefined; url?: string | undefined; severity?: string | number | undefined; duration?: string | number | undefined; created?: string | undefined; dataset?: string | undefined; hash?: string | undefined; ingested?: string | undefined; kind?: string | undefined; module?: string | undefined; original?: string | undefined; provider?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: string | number | undefined; timezone?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; } & {}> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, diff --git a/api_docs/event_log.mdx b/api_docs/event_log.mdx index bfe5735a5b5aa..5e2e0f6c10292 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'eventLog'] --- import eventLogObj from './event_log.devdocs.json'; diff --git a/api_docs/exploratory_view.mdx b/api_docs/exploratory_view.mdx index 2ebfc71ebc060..7315c69e13377 100644 --- a/api_docs/exploratory_view.mdx +++ b/api_docs/exploratory_view.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/exploratoryView title: "exploratoryView" image: https://source.unsplash.com/400x175/?github description: API docs for the exploratoryView plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'exploratoryView'] --- import exploratoryViewObj from './exploratory_view.devdocs.json'; diff --git a/api_docs/expression_error.mdx b/api_docs/expression_error.mdx index 5f952a34778d8..4f23479cfbfc6 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: 2023-04-21 +date: 2023-04-24 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 17b1e1a7dd0c3..3c92eace2b8e3 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: 2023-04-21 +date: 2023-04-24 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 8e23328ac3938..0a35f4158d179 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: 2023-04-21 +date: 2023-04-24 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 2a5739fbf1ce6..92cf80d1ab61a 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: 2023-04-21 +date: 2023-04-24 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 e5641b9c5fd0d..62c32bae5f5f1 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: 2023-04-21 +date: 2023-04-24 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 b8a570a50ce47..8ccde120fd543 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: 2023-04-21 +date: 2023-04-24 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 7ea3ec6b861eb..6432d4714f1e3 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: 2023-04-21 +date: 2023-04-24 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 92e6b7076b109..d8a88c5d95960 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: 2023-04-21 +date: 2023-04-24 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 5641f62c19926..a238dd8036fd1 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: 2023-04-21 +date: 2023-04-24 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 10142b9bf030a..333d1ba0e6264 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: 2023-04-21 +date: 2023-04-24 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 26f353bb29f5f..079a9d1bbced9 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: 2023-04-21 +date: 2023-04-24 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 4961a72b260d7..8af091771bd3e 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: 2023-04-21 +date: 2023-04-24 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 a70dc43040e76..1f6d6a9ba1c68 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: 2023-04-21 +date: 2023-04-24 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 823159e766547..631a20f0bcdf6 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: 2023-04-21 +date: 2023-04-24 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 711def3633bf5..6dedd249971e1 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: 2023-04-21 +date: 2023-04-24 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 0996e6259bf11..fc537a030e52b 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: 2023-04-21 +date: 2023-04-24 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 307f54008c991..624bbf01a981d 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: 2023-04-21 +date: 2023-04-24 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 b3f4dba8b7273..2e7af1d73120f 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'files'] --- import filesObj from './files.devdocs.json'; diff --git a/api_docs/files_management.mdx b/api_docs/files_management.mdx index bbf97bb4e042f..49c9a40ff8946 100644 --- a/api_docs/files_management.mdx +++ b/api_docs/files_management.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/filesManagement title: "filesManagement" image: https://source.unsplash.com/400x175/?github description: API docs for the filesManagement plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'filesManagement'] --- import filesManagementObj from './files_management.devdocs.json'; diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index f1a34342d9ed3..fe95c91406e4c 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: 2023-04-21 +date: 2023-04-24 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 e5180ea54c5f9..37e44d7a88a33 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'globalSearch'] --- import globalSearchObj from './global_search.devdocs.json'; diff --git a/api_docs/guided_onboarding.devdocs.json b/api_docs/guided_onboarding.devdocs.json index cd8f47f8cf277..a73976c10b794 100644 --- a/api_docs/guided_onboarding.devdocs.json +++ b/api_docs/guided_onboarding.devdocs.json @@ -690,7 +690,15 @@ "section": "def-common.GuideStepIds", "text": "GuideStepIds" }, - ") => Promise<{ pluginState: ", + ", params?: ", + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideParams", + "text": "GuideParams" + }, + " | undefined) => Promise<{ pluginState: ", { "pluginId": "guidedOnboarding", "scope": "common", @@ -745,6 +753,28 @@ "deprecated": false, "trackAdoption": false, "isRequired": true + }, + { + "parentPluginId": "guidedOnboarding", + "id": "def-public.GuidedOnboardingApi.completeGuideStep.$3", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "signature": [ + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideParams", + "text": "GuideParams" + }, + " | undefined" + ], + "path": "src/plugins/guided_onboarding/public/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false } ], "returnComment": [] diff --git a/api_docs/guided_onboarding.mdx b/api_docs/guided_onboarding.mdx index d01239408c028..475a8a2c05da3 100644 --- a/api_docs/guided_onboarding.mdx +++ b/api_docs/guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/guidedOnboarding title: "guidedOnboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the guidedOnboarding plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'guidedOnboarding'] --- import guidedOnboardingObj from './guided_onboarding.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/pla | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 56 | 0 | 55 | 0 | +| 57 | 0 | 56 | 0 | ## Client diff --git a/api_docs/home.mdx b/api_docs/home.mdx index 9a3c60b620a1a..f62e10db50461 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'home'] --- import homeObj from './home.devdocs.json'; diff --git a/api_docs/image_embeddable.mdx b/api_docs/image_embeddable.mdx index f46b769442430..3477ece3819e0 100644 --- a/api_docs/image_embeddable.mdx +++ b/api_docs/image_embeddable.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/imageEmbeddable title: "imageEmbeddable" image: https://source.unsplash.com/400x175/?github description: API docs for the imageEmbeddable plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'imageEmbeddable'] --- import imageEmbeddableObj from './image_embeddable.devdocs.json'; diff --git a/api_docs/index_lifecycle_management.mdx b/api_docs/index_lifecycle_management.mdx index 3d5fedb9c28b1..51b3707ff4cda 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: 2023-04-21 +date: 2023-04-24 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 c56e4e1ac00e1..7e62154cabf29 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: 2023-04-21 +date: 2023-04-24 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 2ef0550b3c7ec..f95130c7fda77 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: 2023-04-21 +date: 2023-04-24 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 be420e800114c..627ca307b241f 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: 2023-04-21 +date: 2023-04-24 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 77f9f7caf3d13..5794f30eaf867 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: 2023-04-21 +date: 2023-04-24 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 9d79fbd636e02..ec79dddfe1fdb 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: 2023-04-21 +date: 2023-04-24 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 d38801a007d3a..027622873bb5d 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: 2023-04-21 +date: 2023-04-24 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 d5b19befd4e62..3ba110b4d641e 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/aiops-utils'] --- import kbnAiopsUtilsObj from './kbn_aiops_utils.devdocs.json'; diff --git a/api_docs/kbn_alerting_state_types.mdx b/api_docs/kbn_alerting_state_types.mdx index 9d8f4dce4e4e6..9209559d4c511 100644 --- a/api_docs/kbn_alerting_state_types.mdx +++ b/api_docs/kbn_alerting_state_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerting-state-types title: "@kbn/alerting-state-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerting-state-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerting-state-types'] --- import kbnAlertingStateTypesObj from './kbn_alerting_state_types.devdocs.json'; diff --git a/api_docs/kbn_alerts.mdx b/api_docs/kbn_alerts.mdx index 496c97c35fa87..fd625f04df24b 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts'] --- import kbnAlertsObj from './kbn_alerts.devdocs.json'; diff --git a/api_docs/kbn_alerts_as_data_utils.mdx b/api_docs/kbn_alerts_as_data_utils.mdx index af3a71ef4577b..3529d77abdd16 100644 --- a/api_docs/kbn_alerts_as_data_utils.mdx +++ b/api_docs/kbn_alerts_as_data_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-as-data-utils title: "@kbn/alerts-as-data-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-as-data-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-as-data-utils'] --- import kbnAlertsAsDataUtilsObj from './kbn_alerts_as_data_utils.devdocs.json'; diff --git a/api_docs/kbn_alerts_ui_shared.mdx b/api_docs/kbn_alerts_ui_shared.mdx index 73cb3b3c1b270..c39f9c81be753 100644 --- a/api_docs/kbn_alerts_ui_shared.mdx +++ b/api_docs/kbn_alerts_ui_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-alerts-ui-shared title: "@kbn/alerts-ui-shared" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/alerts-ui-shared plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/alerts-ui-shared'] --- import kbnAlertsUiSharedObj from './kbn_alerts_ui_shared.devdocs.json'; diff --git a/api_docs/kbn_analytics.mdx b/api_docs/kbn_analytics.mdx index c41d203f9bad1..90704b67958d8 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics'] --- import kbnAnalyticsObj from './kbn_analytics.devdocs.json'; diff --git a/api_docs/kbn_analytics_client.devdocs.json b/api_docs/kbn_analytics_client.devdocs.json index 82163bbf3b11d..0671e818e1d3f 100644 --- a/api_docs/kbn_analytics_client.devdocs.json +++ b/api_docs/kbn_analytics_client.devdocs.json @@ -762,6 +762,14 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/public/common/lib/telemetry/telemetry_client.ts" + }, { "plugin": "@kbn/core-analytics-browser-mocks", "path": "packages/core/analytics/core-analytics-browser-mocks/src/analytics_service.mock.ts" diff --git a/api_docs/kbn_analytics_client.mdx b/api_docs/kbn_analytics_client.mdx index cdc8ea6d81beb..5bdc58d85dc6f 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: 2023-04-21 +date: 2023-04-24 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 eaf3487e3149d..98e6154aa8a60 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: 2023-04-21 +date: 2023-04-24 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 9110cc32f4c31..5cedf9017c0c3 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: 2023-04-21 +date: 2023-04-24 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 df8a64c48bd63..1900aafa94eb7 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: 2023-04-21 +date: 2023-04-24 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 05865a67ce0b7..506c2ef17b8a8 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-fullstory'] --- import kbnAnalyticsShippersFullstoryObj from './kbn_analytics_shippers_fullstory.devdocs.json'; diff --git a/api_docs/kbn_analytics_shippers_gainsight.mdx b/api_docs/kbn_analytics_shippers_gainsight.mdx index f960174f73859..51e1daf9c0a4c 100644 --- a/api_docs/kbn_analytics_shippers_gainsight.mdx +++ b/api_docs/kbn_analytics_shippers_gainsight.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-analytics-shippers-gainsight title: "@kbn/analytics-shippers-gainsight" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/analytics-shippers-gainsight plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/analytics-shippers-gainsight'] --- import kbnAnalyticsShippersGainsightObj from './kbn_analytics_shippers_gainsight.devdocs.json'; diff --git a/api_docs/kbn_apm_config_loader.mdx b/api_docs/kbn_apm_config_loader.mdx index 8900077fd2ace..28663f15a680c 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: 2023-04-21 +date: 2023-04-24 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 c8432ebc4205c..1b0583f4267b7 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace'] --- import kbnApmSynthtraceObj from './kbn_apm_synthtrace.devdocs.json'; diff --git a/api_docs/kbn_apm_synthtrace_client.devdocs.json b/api_docs/kbn_apm_synthtrace_client.devdocs.json index 10dcb002bbdd0..fe2bbd03ef6ab 100644 --- a/api_docs/kbn_apm_synthtrace_client.devdocs.json +++ b/api_docs/kbn_apm_synthtrace_client.devdocs.json @@ -731,7 +731,7 @@ "label": "error", "description": [], "signature": [ - "({ message, type, groupingName, }: { message: string; type?: string | undefined; groupingName?: string | undefined; }) => ", + "({ message, type }: { message: string; type?: string | undefined; }) => ", "ApmError" ], "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", @@ -743,7 +743,7 @@ "id": "def-common.Instance.error.$1", "type": "Object", "tags": [], - "label": "{\n message,\n type,\n groupingName,\n }", + "label": "{ message, type }", "description": [], "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", "deprecated": false, @@ -773,20 +773,6 @@ "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", "deprecated": false, "trackAdoption": false - }, - { - "parentPluginId": "@kbn/apm-synthtrace-client", - "id": "def-common.Instance.error.$1.groupingName", - "type": "string", - "tags": [], - "label": "groupingName", - "description": [], - "signature": [ - "string | undefined" - ], - "path": "packages/kbn-apm-synthtrace-client/src/lib/apm/instance.ts", - "deprecated": false, - "trackAdoption": false } ] } diff --git a/api_docs/kbn_apm_synthtrace_client.mdx b/api_docs/kbn_apm_synthtrace_client.mdx index b76de3fe835c9..080fc9faa8d69 100644 --- a/api_docs/kbn_apm_synthtrace_client.mdx +++ b/api_docs/kbn_apm_synthtrace_client.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-apm-synthtrace-client title: "@kbn/apm-synthtrace-client" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/apm-synthtrace-client plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/apm-synthtrace-client'] --- import kbnApmSynthtraceClientObj from './kbn_apm_synthtrace_client.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) for ques | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 154 | 0 | 154 | 17 | +| 153 | 0 | 153 | 17 | ## Common diff --git a/api_docs/kbn_apm_utils.mdx b/api_docs/kbn_apm_utils.mdx index 61e2869b4e841..9509d2d37a5f2 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: 2023-04-21 +date: 2023-04-24 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 ac22df442633d..c9bac589f98e1 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/axe-config'] --- import kbnAxeConfigObj from './kbn_axe_config.devdocs.json'; diff --git a/api_docs/kbn_cases_components.mdx b/api_docs/kbn_cases_components.mdx index 738eaa012d294..9609f525002a3 100644 --- a/api_docs/kbn_cases_components.mdx +++ b/api_docs/kbn_cases_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cases-components title: "@kbn/cases-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cases-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cases-components'] --- import kbnCasesComponentsObj from './kbn_cases_components.devdocs.json'; diff --git a/api_docs/kbn_cell_actions.mdx b/api_docs/kbn_cell_actions.mdx index 891ae75c684ea..936150c592f65 100644 --- a/api_docs/kbn_cell_actions.mdx +++ b/api_docs/kbn_cell_actions.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cell-actions title: "@kbn/cell-actions" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cell-actions plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cell-actions'] --- import kbnCellActionsObj from './kbn_cell_actions.devdocs.json'; diff --git a/api_docs/kbn_chart_expressions_common.mdx b/api_docs/kbn_chart_expressions_common.mdx index ea357626d903d..55c5bc95f3389 100644 --- a/api_docs/kbn_chart_expressions_common.mdx +++ b/api_docs/kbn_chart_expressions_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-chart-expressions-common title: "@kbn/chart-expressions-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/chart-expressions-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/chart-expressions-common'] --- import kbnChartExpressionsCommonObj from './kbn_chart_expressions_common.devdocs.json'; diff --git a/api_docs/kbn_chart_icons.mdx b/api_docs/kbn_chart_icons.mdx index d47f7c20a984c..81991f4c3596a 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: 2023-04-21 +date: 2023-04-24 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 b003b37a45c17..3eeac77d30d96 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: 2023-04-21 +date: 2023-04-24 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 6d8ba1ec6ec6d..4210e48cd996c 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: 2023-04-21 +date: 2023-04-24 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 c9b004312e1fc..dd1eeae4185e8 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: 2023-04-21 +date: 2023-04-24 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 23afc0f33949d..674d7849ea278 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cli-dev-mode'] --- import kbnCliDevModeObj from './kbn_cli_dev_mode.devdocs.json'; diff --git a/api_docs/kbn_code_editor.mdx b/api_docs/kbn_code_editor.mdx index 2128bc3924343..2352502cb6168 100644 --- a/api_docs/kbn_code_editor.mdx +++ b/api_docs/kbn_code_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor title: "@kbn/code-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor'] --- import kbnCodeEditorObj from './kbn_code_editor.devdocs.json'; diff --git a/api_docs/kbn_code_editor_mocks.mdx b/api_docs/kbn_code_editor_mocks.mdx index 2c4aa38ed970a..299b419bc62ee 100644 --- a/api_docs/kbn_code_editor_mocks.mdx +++ b/api_docs/kbn_code_editor_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-code-editor-mocks title: "@kbn/code-editor-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/code-editor-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/code-editor-mocks'] --- import kbnCodeEditorMocksObj from './kbn_code_editor_mocks.devdocs.json'; diff --git a/api_docs/kbn_coloring.mdx b/api_docs/kbn_coloring.mdx index 1f2f008178f13..d0b05022cb683 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: 2023-04-21 +date: 2023-04-24 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 0ce1b008677ff..25ba7ec1f2e1f 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: 2023-04-21 +date: 2023-04-24 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 b7e89d00b3240..c9cb8f54bf0b4 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: 2023-04-21 +date: 2023-04-24 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 81635d8b9bd51..11c17046984bd 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/config-schema'] --- import kbnConfigSchemaObj from './kbn_config_schema.devdocs.json'; diff --git a/api_docs/kbn_content_management_content_editor.mdx b/api_docs/kbn_content_management_content_editor.mdx index 1288f3330685d..54ab8505a8c85 100644 --- a/api_docs/kbn_content_management_content_editor.mdx +++ b/api_docs/kbn_content_management_content_editor.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-content-editor title: "@kbn/content-management-content-editor" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-content-editor plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-content-editor'] --- import kbnContentManagementContentEditorObj from './kbn_content_management_content_editor.devdocs.json'; diff --git a/api_docs/kbn_content_management_table_list.devdocs.json b/api_docs/kbn_content_management_table_list.devdocs.json index 937dddb17c4de..8f1e0a68c22ae 100644 --- a/api_docs/kbn_content_management_table_list.devdocs.json +++ b/api_docs/kbn_content_management_table_list.devdocs.json @@ -177,7 +177,7 @@ "CoreStart contract" ], "signature": [ - "{ application: { capabilities: { advancedSettings?: { save: boolean; } | undefined; }; getUrlForApp: (app: string, options: { path: string; }) => string; currentAppId$: ", + "{ application: { capabilities: { [key: string]: Readonly>>; }; getUrlForApp: (app: string, options: { path: string; }) => string; currentAppId$: ", "Observable", "; navigateToUrl: (url: string) => void | Promise; }; notifications: { toasts: { addDanger: (notifyArgs: { title: ", { diff --git a/api_docs/kbn_content_management_table_list.mdx b/api_docs/kbn_content_management_table_list.mdx index 3dfb0e5bff18e..4c8960148995d 100644 --- a/api_docs/kbn_content_management_table_list.mdx +++ b/api_docs/kbn_content_management_table_list.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-content-management-table-list title: "@kbn/content-management-table-list" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/content-management-table-list plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/content-management-table-list'] --- import kbnContentManagementTableListObj from './kbn_content_management_table_list.devdocs.json'; diff --git a/api_docs/kbn_core_analytics_browser.mdx b/api_docs/kbn_core_analytics_browser.mdx index 5562e1bf3070f..61fd9c2863537 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: 2023-04-21 +date: 2023-04-24 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 d81b99987512d..8a3895b535171 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: 2023-04-21 +date: 2023-04-24 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 fb39ec52f5459..685a28bea6242 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: 2023-04-21 +date: 2023-04-24 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 5a1af3d3f7d7d..5db1b0e6e16a6 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: 2023-04-21 +date: 2023-04-24 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 111fe6d8e5318..4272953c4d763 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: 2023-04-21 +date: 2023-04-24 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 d7be4f034346a..e0082181ed949 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: 2023-04-21 +date: 2023-04-24 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 b57a29bb0ad0a..d3f1f8cfa0925 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: 2023-04-21 +date: 2023-04-24 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 00a6fa0cb815d..6ff37b3089b42 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: 2023-04-21 +date: 2023-04-24 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 e1a48c61d3b9b..02a3fd158ec19 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: 2023-04-21 +date: 2023-04-24 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 4bad226894e0c..d6a70b2b0157f 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-application-common'] --- import kbnCoreApplicationCommonObj from './kbn_core_application_common.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_internal.mdx b/api_docs/kbn_core_apps_browser_internal.mdx index 5c042475509cb..d7c038b7a711f 100644 --- a/api_docs/kbn_core_apps_browser_internal.mdx +++ b/api_docs/kbn_core_apps_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-internal title: "@kbn/core-apps-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-internal'] --- import kbnCoreAppsBrowserInternalObj from './kbn_core_apps_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_apps_browser_mocks.mdx b/api_docs/kbn_core_apps_browser_mocks.mdx index 69f9212722cc9..eb6b4e1963417 100644 --- a/api_docs/kbn_core_apps_browser_mocks.mdx +++ b/api_docs/kbn_core_apps_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-browser-mocks title: "@kbn/core-apps-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-browser-mocks'] --- import kbnCoreAppsBrowserMocksObj from './kbn_core_apps_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_apps_server_internal.mdx b/api_docs/kbn_core_apps_server_internal.mdx index 65e3d10226f52..1fcfd9f49e0fe 100644 --- a/api_docs/kbn_core_apps_server_internal.mdx +++ b/api_docs/kbn_core_apps_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-apps-server-internal title: "@kbn/core-apps-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-apps-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-apps-server-internal'] --- import kbnCoreAppsServerInternalObj from './kbn_core_apps_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_base_browser_mocks.mdx b/api_docs/kbn_core_base_browser_mocks.mdx index 86f50bf667604..7d8bb7252a866 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: 2023-04-21 +date: 2023-04-24 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.mdx b/api_docs/kbn_core_base_common.mdx index cf75d243d2b43..69cc08dec9190 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-base-common'] --- import kbnCoreBaseCommonObj from './kbn_core_base_common.devdocs.json'; diff --git a/api_docs/kbn_core_base_server_internal.mdx b/api_docs/kbn_core_base_server_internal.mdx index 16fa31f01e292..eef24003ab660 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: 2023-04-21 +date: 2023-04-24 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 4ca1597fcfadc..58aff21c3f633 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: 2023-04-21 +date: 2023-04-24 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 a86cc6571c831..14f3d8712b6f3 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: 2023-04-21 +date: 2023-04-24 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 027e44a2e349d..d0b5db8e3f5b1 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: 2023-04-21 +date: 2023-04-24 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 492519d0fef3a..ced47f174e1bf 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: 2023-04-21 +date: 2023-04-24 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 a8ef5c7e24843..236d416f8d3f5 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: 2023-04-21 +date: 2023-04-24 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 cf04b0d4146b0..c286a69723583 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: 2023-04-21 +date: 2023-04-24 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 c3ba2d14d0f84..c4201d23a52f5 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: 2023-04-21 +date: 2023-04-24 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 69c93fed8bdf6..5524f5d6c7f3a 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: 2023-04-21 +date: 2023-04-24 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_custom_branding_browser.mdx b/api_docs/kbn_core_custom_branding_browser.mdx index 3447e44fdeb14..c06544f094756 100644 --- a/api_docs/kbn_core_custom_branding_browser.mdx +++ b/api_docs/kbn_core_custom_branding_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser title: "@kbn/core-custom-branding-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser'] --- import kbnCoreCustomBrandingBrowserObj from './kbn_core_custom_branding_browser.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_internal.mdx b/api_docs/kbn_core_custom_branding_browser_internal.mdx index 512e0a4658e11..47db10d316e4b 100644 --- a/api_docs/kbn_core_custom_branding_browser_internal.mdx +++ b/api_docs/kbn_core_custom_branding_browser_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-internal title: "@kbn/core-custom-branding-browser-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-internal'] --- import kbnCoreCustomBrandingBrowserInternalObj from './kbn_core_custom_branding_browser_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_browser_mocks.mdx b/api_docs/kbn_core_custom_branding_browser_mocks.mdx index 57674f1a86f54..74eb25b3e0989 100644 --- a/api_docs/kbn_core_custom_branding_browser_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-browser-mocks title: "@kbn/core-custom-branding-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-browser-mocks'] --- import kbnCoreCustomBrandingBrowserMocksObj from './kbn_core_custom_branding_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_common.mdx b/api_docs/kbn_core_custom_branding_common.mdx index 3271ecbc40835..2852e74f1d272 100644 --- a/api_docs/kbn_core_custom_branding_common.mdx +++ b/api_docs/kbn_core_custom_branding_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-common title: "@kbn/core-custom-branding-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-common'] --- import kbnCoreCustomBrandingCommonObj from './kbn_core_custom_branding_common.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server.mdx b/api_docs/kbn_core_custom_branding_server.mdx index 947830e94c31a..6b39d83563325 100644 --- a/api_docs/kbn_core_custom_branding_server.mdx +++ b/api_docs/kbn_core_custom_branding_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server title: "@kbn/core-custom-branding-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server'] --- import kbnCoreCustomBrandingServerObj from './kbn_core_custom_branding_server.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_internal.mdx b/api_docs/kbn_core_custom_branding_server_internal.mdx index fed153ea6f806..efcb0ef0d682e 100644 --- a/api_docs/kbn_core_custom_branding_server_internal.mdx +++ b/api_docs/kbn_core_custom_branding_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-internal title: "@kbn/core-custom-branding-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-internal'] --- import kbnCoreCustomBrandingServerInternalObj from './kbn_core_custom_branding_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_custom_branding_server_mocks.mdx b/api_docs/kbn_core_custom_branding_server_mocks.mdx index 14c84336e55d8..1b84c3249cedb 100644 --- a/api_docs/kbn_core_custom_branding_server_mocks.mdx +++ b/api_docs/kbn_core_custom_branding_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-custom-branding-server-mocks title: "@kbn/core-custom-branding-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-custom-branding-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-custom-branding-server-mocks'] --- import kbnCoreCustomBrandingServerMocksObj from './kbn_core_custom_branding_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_deprecations_browser.mdx b/api_docs/kbn_core_deprecations_browser.mdx index f5dac83c7c859..d9183c76749eb 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: 2023-04-21 +date: 2023-04-24 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 bd675bc3f02fb..2dabde1158feb 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: 2023-04-21 +date: 2023-04-24 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 41ca6aa3b4e82..1f150624000c0 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: 2023-04-21 +date: 2023-04-24 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 5d235b665bccf..f6098418f65b8 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: 2023-04-21 +date: 2023-04-24 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 e11a7361be6fe..18f41498f2022 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: 2023-04-21 +date: 2023-04-24 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 86023b4693bda..4da7cae29afc1 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: 2023-04-21 +date: 2023-04-24 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 1b1996813ec5e..7efaa1d260ede 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: 2023-04-21 +date: 2023-04-24 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 014a6cff4fb8f..3a8b4f7733b89 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: 2023-04-21 +date: 2023-04-24 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 8a827ee08a3cb..6d30b7f184a66 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: 2023-04-21 +date: 2023-04-24 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 cf25c21fc3e7b..7e04c291cf2e5 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: 2023-04-21 +date: 2023-04-24 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 d81566d93b87b..bd6d7e5360355 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: 2023-04-21 +date: 2023-04-24 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 52c845ba3e159..108a226e77328 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: 2023-04-21 +date: 2023-04-24 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 25bdcf097fc8f..36da5cccdf92f 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: 2023-04-21 +date: 2023-04-24 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 d3e52e4498e97..a54b680f5d90d 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: 2023-04-21 +date: 2023-04-24 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 d7116ffefd9e3..22c91cf01d375 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: 2023-04-21 +date: 2023-04-24 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 8763333e3c1ca..50e70544b210b 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: 2023-04-21 +date: 2023-04-24 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 c72123128deb8..f538a5f8e0b4e 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: 2023-04-21 +date: 2023-04-24 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 2bca835abcc60..8f1461db2e66f 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: 2023-04-21 +date: 2023-04-24 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 e0133ab2eceac..77a377a7db9c4 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: 2023-04-21 +date: 2023-04-24 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 f4285539090c7..d7dcc38efe471 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: 2023-04-21 +date: 2023-04-24 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 e88005161eee4..d1ac9f6597bd6 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: 2023-04-21 +date: 2023-04-24 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 9bb759d225d18..4d4c0cdc35fd7 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: 2023-04-21 +date: 2023-04-24 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 391aab7e7c207..d81bd38aa1916 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: 2023-04-21 +date: 2023-04-24 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 b0a32a23bc8fd..05d5f2ae35765 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: 2023-04-21 +date: 2023-04-24 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 d044bcd313597..68eb012274a21 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: 2023-04-21 +date: 2023-04-24 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 3185c3568f7cd..4bdeef98bbfde 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: 2023-04-21 +date: 2023-04-24 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 6d4e823e86bc7..d94568e3ed0e6 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: 2023-04-21 +date: 2023-04-24 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 2379a6b30140a..306f52219527d 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: 2023-04-21 +date: 2023-04-24 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 3a8785ce36e04..ab7a2167ee8d3 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: 2023-04-21 +date: 2023-04-24 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 cd9f5e1f32fae..3a484f5c5bb94 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: 2023-04-21 +date: 2023-04-24 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 030701d5c5535..4c6f719e62bb6 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: 2023-04-21 +date: 2023-04-24 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 3041948b6c08e..867ab6116747a 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: 2023-04-21 +date: 2023-04-24 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_request_handler_context_server.mdx b/api_docs/kbn_core_http_request_handler_context_server.mdx index 6c55bb1acc35f..ad72f4f7242bc 100644 --- a/api_docs/kbn_core_http_request_handler_context_server.mdx +++ b/api_docs/kbn_core_http_request_handler_context_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-request-handler-context-server title: "@kbn/core-http-request-handler-context-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-request-handler-context-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-request-handler-context-server'] --- import kbnCoreHttpRequestHandlerContextServerObj from './kbn_core_http_request_handler_context_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server.mdx b/api_docs/kbn_core_http_resources_server.mdx index 1fc1939592f1d..8bca969b22064 100644 --- a/api_docs/kbn_core_http_resources_server.mdx +++ b/api_docs/kbn_core_http_resources_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server title: "@kbn/core-http-resources-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server'] --- import kbnCoreHttpResourcesServerObj from './kbn_core_http_resources_server.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_internal.mdx b/api_docs/kbn_core_http_resources_server_internal.mdx index 1ce4c26ce2a2e..408b53256f7db 100644 --- a/api_docs/kbn_core_http_resources_server_internal.mdx +++ b/api_docs/kbn_core_http_resources_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-internal title: "@kbn/core-http-resources-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-internal'] --- import kbnCoreHttpResourcesServerInternalObj from './kbn_core_http_resources_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_http_resources_server_mocks.mdx b/api_docs/kbn_core_http_resources_server_mocks.mdx index c4dd257493cdb..c87dfa9f9fccf 100644 --- a/api_docs/kbn_core_http_resources_server_mocks.mdx +++ b/api_docs/kbn_core_http_resources_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-http-resources-server-mocks title: "@kbn/core-http-resources-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-http-resources-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-http-resources-server-mocks'] --- import kbnCoreHttpResourcesServerMocksObj from './kbn_core_http_resources_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 7baa129a75c92..f165267f56e33 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: 2023-04-21 +date: 2023-04-24 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 9115db4edf6cc..e9e7dc64f0942 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: 2023-04-21 +date: 2023-04-24 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 152820cbb488b..f0cdaa85851dc 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: 2023-04-21 +date: 2023-04-24 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 1dc544d47e781..f38f74d167a98 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: 2023-04-21 +date: 2023-04-24 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 12f4523bca3e3..c59347545a328 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: 2023-04-21 +date: 2023-04-24 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 290dc43e0f6a4..c38585619f6c0 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: 2023-04-21 +date: 2023-04-24 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 1892f31f90652..0fe31a6d87fa1 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: 2023-04-21 +date: 2023-04-24 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 8cd6df89cee80..a9a56e81b982d 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: 2023-04-21 +date: 2023-04-24 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 71388b8df33ef..ec02bcea6b7f7 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: 2023-04-21 +date: 2023-04-24 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 b46f5cadc7bf9..1a2c897736245 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: 2023-04-21 +date: 2023-04-24 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_mocks.mdx b/api_docs/kbn_core_injected_metadata_browser_mocks.mdx index 188df6cc9052d..3c162c9189752 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: 2023-04-21 +date: 2023-04-24 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 75b8dc9819d5a..494208c4c5914 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: 2023-04-21 +date: 2023-04-24 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 a9e58cbc2c4fb..a037d65732f72 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: 2023-04-21 +date: 2023-04-24 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_lifecycle_browser.mdx b/api_docs/kbn_core_lifecycle_browser.mdx index d29ee50a427bf..99067d79f0a9f 100644 --- a/api_docs/kbn_core_lifecycle_browser.mdx +++ b/api_docs/kbn_core_lifecycle_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser title: "@kbn/core-lifecycle-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser'] --- import kbnCoreLifecycleBrowserObj from './kbn_core_lifecycle_browser.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_browser_mocks.mdx b/api_docs/kbn_core_lifecycle_browser_mocks.mdx index a3f5c62d35e87..534bb78d4eaba 100644 --- a/api_docs/kbn_core_lifecycle_browser_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-browser-mocks title: "@kbn/core-lifecycle-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-browser-mocks'] --- import kbnCoreLifecycleBrowserMocksObj from './kbn_core_lifecycle_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server.mdx b/api_docs/kbn_core_lifecycle_server.mdx index 336ba1167e471..81ac149482e3c 100644 --- a/api_docs/kbn_core_lifecycle_server.mdx +++ b/api_docs/kbn_core_lifecycle_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server title: "@kbn/core-lifecycle-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server'] --- import kbnCoreLifecycleServerObj from './kbn_core_lifecycle_server.devdocs.json'; diff --git a/api_docs/kbn_core_lifecycle_server_mocks.mdx b/api_docs/kbn_core_lifecycle_server_mocks.mdx index ead03154da97c..26ba420c08d5c 100644 --- a/api_docs/kbn_core_lifecycle_server_mocks.mdx +++ b/api_docs/kbn_core_lifecycle_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-lifecycle-server-mocks title: "@kbn/core-lifecycle-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-lifecycle-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-lifecycle-server-mocks'] --- import kbnCoreLifecycleServerMocksObj from './kbn_core_lifecycle_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_browser_mocks.mdx b/api_docs/kbn_core_logging_browser_mocks.mdx index 70dcc85174431..9ba0f825772fa 100644 --- a/api_docs/kbn_core_logging_browser_mocks.mdx +++ b/api_docs/kbn_core_logging_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-browser-mocks title: "@kbn/core-logging-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-browser-mocks'] --- import kbnCoreLoggingBrowserMocksObj from './kbn_core_logging_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_logging_common_internal.mdx b/api_docs/kbn_core_logging_common_internal.mdx index eaaab07d66216..a960a65461e1d 100644 --- a/api_docs/kbn_core_logging_common_internal.mdx +++ b/api_docs/kbn_core_logging_common_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-logging-common-internal title: "@kbn/core-logging-common-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-logging-common-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-logging-common-internal'] --- import kbnCoreLoggingCommonInternalObj from './kbn_core_logging_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_logging_server.mdx b/api_docs/kbn_core_logging_server.mdx index 764140b0a5e6c..3fd09f6263c27 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: 2023-04-21 +date: 2023-04-24 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 96a65db189368..f3acbc7fc8045 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: 2023-04-21 +date: 2023-04-24 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 7edad6a24183a..c3ea743d26d84 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: 2023-04-21 +date: 2023-04-24 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 1bed7b5faea0a..71e8faae9eed4 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: 2023-04-21 +date: 2023-04-24 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 f8d4fffbfe021..2eabbc2bacc62 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: 2023-04-21 +date: 2023-04-24 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 daf89498a1cfc..f37d52033dd7a 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: 2023-04-21 +date: 2023-04-24 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 2faf2f55b3dae..a7217eaf16db7 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: 2023-04-21 +date: 2023-04-24 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 9a208c0343c78..59ea42f319e9d 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: 2023-04-21 +date: 2023-04-24 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 22a3bada69676..466745e2ce499 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: 2023-04-21 +date: 2023-04-24 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 a85a0e176e836..98986e42b5bb0 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: 2023-04-21 +date: 2023-04-24 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 0418f4ca39d60..d8b7087087930 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: 2023-04-21 +date: 2023-04-24 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 fef44b08e7f61..b45a40d461b52 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: 2023-04-21 +date: 2023-04-24 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 5b4c9e57d0c5e..c8536a941c46f 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: 2023-04-21 +date: 2023-04-24 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 1c53d4f1151a2..7146b20a88240 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: 2023-04-21 +date: 2023-04-24 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 ac5a768548470..2518700bd2a9d 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: 2023-04-21 +date: 2023-04-24 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 6538de0675581..38f32ad1bd2c1 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: 2023-04-21 +date: 2023-04-24 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 2be250444e13d..d158e2d4ff953 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: 2023-04-21 +date: 2023-04-24 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 36a17d070c6e5..e2c79208c0674 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: 2023-04-21 +date: 2023-04-24 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_plugins_browser.mdx b/api_docs/kbn_core_plugins_browser.mdx index 7a0bd26bc2490..8d655eb267f5a 100644 --- a/api_docs/kbn_core_plugins_browser.mdx +++ b/api_docs/kbn_core_plugins_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser title: "@kbn/core-plugins-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser'] --- import kbnCorePluginsBrowserObj from './kbn_core_plugins_browser.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_browser_mocks.mdx b/api_docs/kbn_core_plugins_browser_mocks.mdx index 94f1a573c6e08..77ea3d979a1fa 100644 --- a/api_docs/kbn_core_plugins_browser_mocks.mdx +++ b/api_docs/kbn_core_plugins_browser_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-browser-mocks title: "@kbn/core-plugins-browser-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-browser-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-browser-mocks'] --- import kbnCorePluginsBrowserMocksObj from './kbn_core_plugins_browser_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server.mdx b/api_docs/kbn_core_plugins_server.mdx index 20d7ec12378e1..44b22b20623c4 100644 --- a/api_docs/kbn_core_plugins_server.mdx +++ b/api_docs/kbn_core_plugins_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server title: "@kbn/core-plugins-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server'] --- import kbnCorePluginsServerObj from './kbn_core_plugins_server.devdocs.json'; diff --git a/api_docs/kbn_core_plugins_server_mocks.mdx b/api_docs/kbn_core_plugins_server_mocks.mdx index 1186e3108b3e3..c032049f2dd4f 100644 --- a/api_docs/kbn_core_plugins_server_mocks.mdx +++ b/api_docs/kbn_core_plugins_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-plugins-server-mocks title: "@kbn/core-plugins-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-plugins-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-plugins-server-mocks'] --- import kbnCorePluginsServerMocksObj from './kbn_core_plugins_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_preboot_server.mdx b/api_docs/kbn_core_preboot_server.mdx index 9e81829a0ef67..08eca2b502206 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: 2023-04-21 +date: 2023-04-24 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 920f32db8a28e..971dc60fbd402 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: 2023-04-21 +date: 2023-04-24 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 ff75e7da8fd40..f7cb4f618bbf9 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: 2023-04-21 +date: 2023-04-24 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_rendering_server_internal.mdx b/api_docs/kbn_core_rendering_server_internal.mdx index 5d8f7df7ac176..91d2f9cd5b3a7 100644 --- a/api_docs/kbn_core_rendering_server_internal.mdx +++ b/api_docs/kbn_core_rendering_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-internal title: "@kbn/core-rendering-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-internal'] --- import kbnCoreRenderingServerInternalObj from './kbn_core_rendering_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_rendering_server_mocks.mdx b/api_docs/kbn_core_rendering_server_mocks.mdx index fc7ce48733faa..e114fbfb6c870 100644 --- a/api_docs/kbn_core_rendering_server_mocks.mdx +++ b/api_docs/kbn_core_rendering_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-rendering-server-mocks title: "@kbn/core-rendering-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-rendering-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-rendering-server-mocks'] --- import kbnCoreRenderingServerMocksObj from './kbn_core_rendering_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_root_server_internal.mdx b/api_docs/kbn_core_root_server_internal.mdx index 66f6d3135417b..12234cdb3c57c 100644 --- a/api_docs/kbn_core_root_server_internal.mdx +++ b/api_docs/kbn_core_root_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-root-server-internal title: "@kbn/core-root-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-root-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-root-server-internal'] --- import kbnCoreRootServerInternalObj from './kbn_core_root_server_internal.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 814eebd1bcbfa..2ae4ebd0c693b 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: 2023-04-21 +date: 2023-04-24 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 4c5f84aee2327..55646f1f382b6 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: 2023-04-21 +date: 2023-04-24 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 3f8241f9ab849..165d452af821e 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: 2023-04-21 +date: 2023-04-24 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 72ad46898861d..99c35f7983158 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: 2023-04-21 +date: 2023-04-24 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 7e7ccf32b70ab..1978b36015f7e 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: 2023-04-21 +date: 2023-04-24 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 76f857265d07b..79c73b9220d0c 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: 2023-04-21 +date: 2023-04-24 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 395382f11c5a6..10eb5868ecf47 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: 2023-04-21 +date: 2023-04-24 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 e30fc42495448..4615be9c11ce1 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: 2023-04-21 +date: 2023-04-24 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 7797116f838b6..a37c978cfb343 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: 2023-04-21 +date: 2023-04-24 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 c7820ecbca374..706710e2cb824 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: 2023-04-21 +date: 2023-04-24 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 34d40c1a0e34b..e0122dffcacb0 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: 2023-04-21 +date: 2023-04-24 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 95f19589a3e7b..08cc43ae6a54d 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: 2023-04-21 +date: 2023-04-24 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 1237af41a106a..49dcee745ce38 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: 2023-04-21 +date: 2023-04-24 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 2b1be8ab9ccf3..612f215e8d922 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: 2023-04-21 +date: 2023-04-24 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 01110761151d4..391665fd14f8c 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: 2023-04-21 +date: 2023-04-24 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 c1570d44d6a4e..8f7af4ecd077d 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: 2023-04-21 +date: 2023-04-24 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 b4d9023205b5b..0a1e5d1292b0a 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: 2023-04-21 +date: 2023-04-24 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 68054fd030ba7..b99ce47f08e8a 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: 2023-04-21 +date: 2023-04-24 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.mdx b/api_docs/kbn_core_status_common.mdx index 0099c87fe71c1..de9b1e18cae5d 100644 --- a/api_docs/kbn_core_status_common.mdx +++ b/api_docs/kbn_core_status_common.mdx @@ -8,7 +8,7 @@ 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common'] --- import kbnCoreStatusCommonObj from './kbn_core_status_common.devdocs.json'; diff --git a/api_docs/kbn_core_status_common_internal.mdx b/api_docs/kbn_core_status_common_internal.mdx index 942ceea0dddb0..301494e7b2388 100644 --- a/api_docs/kbn_core_status_common_internal.mdx +++ b/api_docs/kbn_core_status_common_internal.mdx @@ -8,7 +8,7 @@ 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-common-internal'] --- import kbnCoreStatusCommonInternalObj from './kbn_core_status_common_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server.mdx b/api_docs/kbn_core_status_server.mdx index b9013fd6ca323..df15953abc315 100644 --- a/api_docs/kbn_core_status_server.mdx +++ b/api_docs/kbn_core_status_server.mdx @@ -8,7 +8,7 @@ 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server'] --- import kbnCoreStatusServerObj from './kbn_core_status_server.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_internal.mdx b/api_docs/kbn_core_status_server_internal.mdx index e9c7d05fc7a4f..075a12e1c6f24 100644 --- a/api_docs/kbn_core_status_server_internal.mdx +++ b/api_docs/kbn_core_status_server_internal.mdx @@ -8,7 +8,7 @@ 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-internal'] --- import kbnCoreStatusServerInternalObj from './kbn_core_status_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_status_server_mocks.mdx b/api_docs/kbn_core_status_server_mocks.mdx index 0c6549c2c40a8..d47e6f46490f1 100644 --- a/api_docs/kbn_core_status_server_mocks.mdx +++ b/api_docs/kbn_core_status_server_mocks.mdx @@ -8,7 +8,7 @@ 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-status-server-mocks'] --- import kbnCoreStatusServerMocksObj from './kbn_core_status_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_deprecations_getters.mdx b/api_docs/kbn_core_test_helpers_deprecations_getters.mdx index 0148b222dc307..a38afc5d4cc28 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: 2023-04-21 +date: 2023-04-24 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 5c3b407e8d7e1..ef13c55e07c61 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: 2023-04-21 +date: 2023-04-24 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_test_helpers_kbn_server.mdx b/api_docs/kbn_core_test_helpers_kbn_server.mdx index 9f82264d3416a..cf75dcf5478b4 100644 --- a/api_docs/kbn_core_test_helpers_kbn_server.mdx +++ b/api_docs/kbn_core_test_helpers_kbn_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-kbn-server title: "@kbn/core-test-helpers-kbn-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-kbn-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-kbn-server'] --- import kbnCoreTestHelpersKbnServerObj from './kbn_core_test_helpers_kbn_server.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx index 361d7ee4abaef..f11a869c92ee1 100644 --- a/api_docs/kbn_core_test_helpers_so_type_serializer.mdx +++ b/api_docs/kbn_core_test_helpers_so_type_serializer.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-so-type-serializer title: "@kbn/core-test-helpers-so-type-serializer" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-so-type-serializer plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-so-type-serializer'] --- import kbnCoreTestHelpersSoTypeSerializerObj from './kbn_core_test_helpers_so_type_serializer.devdocs.json'; diff --git a/api_docs/kbn_core_test_helpers_test_utils.mdx b/api_docs/kbn_core_test_helpers_test_utils.mdx index 729bf46fe8b67..484dd22728fbc 100644 --- a/api_docs/kbn_core_test_helpers_test_utils.mdx +++ b/api_docs/kbn_core_test_helpers_test_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-test-helpers-test-utils title: "@kbn/core-test-helpers-test-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-test-helpers-test-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-test-helpers-test-utils'] --- import kbnCoreTestHelpersTestUtilsObj from './kbn_core_test_helpers_test_utils.devdocs.json'; diff --git a/api_docs/kbn_core_theme_browser.mdx b/api_docs/kbn_core_theme_browser.mdx index 59ca3b555e77c..63cd2de86d6e4 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: 2023-04-21 +date: 2023-04-24 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 d8e9d7d490f07..54a121b8df78a 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: 2023-04-21 +date: 2023-04-24 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 fcbeaaf620f5b..a21b5fa4ab454 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: 2023-04-21 +date: 2023-04-24 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 db1ea4bacbeff..e741c75cc66b0 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: 2023-04-21 +date: 2023-04-24 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 4bd232bbc6d3d..92c21046ca2bb 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: 2023-04-21 +date: 2023-04-24 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 50362b87de095..9b8babfa1fcf3 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: 2023-04-21 +date: 2023-04-24 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 955203aeab79b..b95f699d6d050 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: 2023-04-21 +date: 2023-04-24 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_ui_settings_server.mdx b/api_docs/kbn_core_ui_settings_server.mdx index 5e5c93b62137f..a88c23c489891 100644 --- a/api_docs/kbn_core_ui_settings_server.mdx +++ b/api_docs/kbn_core_ui_settings_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server title: "@kbn/core-ui-settings-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server'] --- import kbnCoreUiSettingsServerObj from './kbn_core_ui_settings_server.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_internal.mdx b/api_docs/kbn_core_ui_settings_server_internal.mdx index 586bb305927ea..06432816c1788 100644 --- a/api_docs/kbn_core_ui_settings_server_internal.mdx +++ b/api_docs/kbn_core_ui_settings_server_internal.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-internal title: "@kbn/core-ui-settings-server-internal" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-internal plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-internal'] --- import kbnCoreUiSettingsServerInternalObj from './kbn_core_ui_settings_server_internal.devdocs.json'; diff --git a/api_docs/kbn_core_ui_settings_server_mocks.mdx b/api_docs/kbn_core_ui_settings_server_mocks.mdx index 8bde395eb141e..e1477ab3eeb87 100644 --- a/api_docs/kbn_core_ui_settings_server_mocks.mdx +++ b/api_docs/kbn_core_ui_settings_server_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-core-ui-settings-server-mocks title: "@kbn/core-ui-settings-server-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/core-ui-settings-server-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/core-ui-settings-server-mocks'] --- import kbnCoreUiSettingsServerMocksObj from './kbn_core_ui_settings_server_mocks.devdocs.json'; diff --git a/api_docs/kbn_core_usage_data_server.mdx b/api_docs/kbn_core_usage_data_server.mdx index ac875acccae8d..914a5468667b5 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: 2023-04-21 +date: 2023-04-24 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 e9e8099f4692e..b5596d8ce5bc7 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: 2023-04-21 +date: 2023-04-24 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 d98b4e73ab7f0..c5fc08f39d31e 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: 2023-04-21 +date: 2023-04-24 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 a367c613df999..ff42ee5669ec3 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: 2023-04-21 +date: 2023-04-24 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 0b4c15845ebc7..6af342c88de2c 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/crypto-browser'] --- import kbnCryptoBrowserObj from './kbn_crypto_browser.devdocs.json'; diff --git a/api_docs/kbn_cypress_config.mdx b/api_docs/kbn_cypress_config.mdx index e951de4adfb35..845a8d13f6268 100644 --- a/api_docs/kbn_cypress_config.mdx +++ b/api_docs/kbn_cypress_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-cypress-config title: "@kbn/cypress-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/cypress-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/cypress-config'] --- import kbnCypressConfigObj from './kbn_cypress_config.devdocs.json'; diff --git a/api_docs/kbn_datemath.mdx b/api_docs/kbn_datemath.mdx index 3d864f429b0c3..3b0b33c77b227 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: 2023-04-21 +date: 2023-04-24 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 af952748b1e5a..afafe20cb2e1e 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: 2023-04-21 +date: 2023-04-24 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 b66e4a59edd0e..4de5774e12693 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: 2023-04-21 +date: 2023-04-24 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 74b88642009bd..683f4f5f7821b 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: 2023-04-21 +date: 2023-04-24 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 df18c4a7ae5e6..36602e1f31d55 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: 2023-04-21 +date: 2023-04-24 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 237b608f7aa52..3aaf17560db47 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: 2023-04-21 +date: 2023-04-24 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 d12f4055fc03a..b1465811e2295 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/docs-utils'] --- import kbnDocsUtilsObj from './kbn_docs_utils.devdocs.json'; diff --git a/api_docs/kbn_dom_drag_drop.mdx b/api_docs/kbn_dom_drag_drop.mdx index a9c6b2c6c0a28..ead5751fa35c3 100644 --- a/api_docs/kbn_dom_drag_drop.mdx +++ b/api_docs/kbn_dom_drag_drop.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-dom-drag-drop title: "@kbn/dom-drag-drop" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/dom-drag-drop plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/dom-drag-drop'] --- import kbnDomDragDropObj from './kbn_dom_drag_drop.devdocs.json'; diff --git a/api_docs/kbn_ebt_tools.mdx b/api_docs/kbn_ebt_tools.mdx index f7d904bd7bce7..17263518fb4d1 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ebt-tools'] --- import kbnEbtToolsObj from './kbn_ebt_tools.devdocs.json'; diff --git a/api_docs/kbn_ecs.mdx b/api_docs/kbn_ecs.mdx index a070106c4a4f4..6b880006797c0 100644 --- a/api_docs/kbn_ecs.mdx +++ b/api_docs/kbn_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs title: "@kbn/ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs'] --- import kbnEcsObj from './kbn_ecs.devdocs.json'; diff --git a/api_docs/kbn_ecs_data_quality_dashboard.mdx b/api_docs/kbn_ecs_data_quality_dashboard.mdx index d6933f82a8a56..0b8d0d939e4ae 100644 --- a/api_docs/kbn_ecs_data_quality_dashboard.mdx +++ b/api_docs/kbn_ecs_data_quality_dashboard.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ecs-data-quality-dashboard title: "@kbn/ecs-data-quality-dashboard" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ecs-data-quality-dashboard plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ecs-data-quality-dashboard'] --- import kbnEcsDataQualityDashboardObj from './kbn_ecs_data_quality_dashboard.devdocs.json'; diff --git a/api_docs/kbn_es.mdx b/api_docs/kbn_es.mdx index b90987c9e70ef..2969a379e7d3c 100644 --- a/api_docs/kbn_es.mdx +++ b/api_docs/kbn_es.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es title: "@kbn/es" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es'] --- import kbnEsObj from './kbn_es.devdocs.json'; diff --git a/api_docs/kbn_es_archiver.mdx b/api_docs/kbn_es_archiver.mdx index 9664b87b6910b..eaf29c3077a8b 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: 2023-04-21 +date: 2023-04-24 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 8a8c25e5981a8..ab41505be21e9 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-errors'] --- import kbnEsErrorsObj from './kbn_es_errors.devdocs.json'; diff --git a/api_docs/kbn_es_query.devdocs.json b/api_docs/kbn_es_query.devdocs.json index 4aac051beec95..02a3068d88627 100644 --- a/api_docs/kbn_es_query.devdocs.json +++ b/api_docs/kbn_es_query.devdocs.json @@ -4243,8 +4243,6 @@ "FilterMetaParams", " | undefined; from?: string | number | undefined; to?: string | number | undefined; gt?: string | number | undefined; lt?: string | number | undefined; gte?: string | number | undefined; lte?: string | number | undefined; format?: string | undefined; } | { query: ", "FilterMetaParams", - " | undefined; length: number; toString(): string; toLocaleString(): string; pop(): number | undefined; push(...items: number[]): number; concat(...items: ConcatArray[]): number[]; concat(...items: (number | ConcatArray)[]): number[]; join(separator?: string | undefined): string; reverse(): number[]; shift(): number | undefined; slice(start?: number | undefined, end?: number | undefined): number[]; sort(compareFn?: ((a: number, b: number) => number) | undefined): number[]; splice(start: number, deleteCount?: number | undefined): number[]; splice(start: number, deleteCount: number, ...items: number[]): number[]; unshift(...items: number[]): number; indexOf(searchElement: number, fromIndex?: number | undefined): number; lastIndexOf(searchElement: number, fromIndex?: number | undefined): number; every(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; some(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any): void; map(callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any): U[]; filter(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number[]; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; find(predicate: (this: void, value: number, index: number, obj: number[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number | undefined; findIndex(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number; fill(value: number, start?: number | undefined, end?: number | undefined): number[]; copyWithin(target: number, start: number, end?: number | undefined): number[]; entries(): IterableIterator<[number, number]>; keys(): IterableIterator; values(): IterableIterator; includes(searchElement: number, fromIndex?: number | undefined): boolean; flatMap(callback: (this: This, value: number, index: number, array: number[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; [Symbol.iterator](): IterableIterator; [Symbol.unscopables](): { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }; at(index: number): number | undefined; } | { query: ", - "FilterMetaParams", " | undefined; $state?: { store: ", { "pluginId": "@kbn/es-query", @@ -4263,6 +4261,8 @@ }, "; } | { query: ", "FilterMetaParams", + " | undefined; length: number; toString(): string; toLocaleString(): string; pop(): number | undefined; push(...items: number[]): number; concat(...items: ConcatArray[]): number[]; concat(...items: (number | ConcatArray)[]): number[]; join(separator?: string | undefined): string; reverse(): number[]; shift(): number | undefined; slice(start?: number | undefined, end?: number | undefined): number[]; sort(compareFn?: ((a: number, b: number) => number) | undefined): number[]; splice(start: number, deleteCount?: number | undefined): number[]; splice(start: number, deleteCount: number, ...items: number[]): number[]; unshift(...items: number[]): number; indexOf(searchElement: number, fromIndex?: number | undefined): number; lastIndexOf(searchElement: number, fromIndex?: number | undefined): number; every(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): this is S[]; every(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; some(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): boolean; forEach(callbackfn: (value: number, index: number, array: number[]) => void, thisArg?: any): void; map(callbackfn: (value: number, index: number, array: number[]) => U, thisArg?: any): U[]; filter(predicate: (value: number, index: number, array: number[]) => value is S, thisArg?: any): S[]; filter(predicate: (value: number, index: number, array: number[]) => unknown, thisArg?: any): number[]; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduce(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduce(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number): number; reduceRight(callbackfn: (previousValue: number, currentValue: number, currentIndex: number, array: number[]) => number, initialValue: number): number; reduceRight(callbackfn: (previousValue: U, currentValue: number, currentIndex: number, array: number[]) => U, initialValue: U): U; find(predicate: (this: void, value: number, index: number, obj: number[]) => value is S, thisArg?: any): S | undefined; find(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number | undefined; findIndex(predicate: (value: number, index: number, obj: number[]) => unknown, thisArg?: any): number; fill(value: number, start?: number | undefined, end?: number | undefined): number[]; copyWithin(target: number, start: number, end?: number | undefined): number[]; entries(): IterableIterator<[number, number]>; keys(): IterableIterator; values(): IterableIterator; includes(searchElement: number, fromIndex?: number | undefined): boolean; flatMap(callback: (this: This, value: number, index: number, array: number[]) => U | readonly U[], thisArg?: This | undefined): U[]; flat(this: A, depth?: D | undefined): FlatArray[]; [Symbol.iterator](): IterableIterator; [Symbol.unscopables](): { copyWithin: boolean; entries: boolean; fill: boolean; find: boolean; findIndex: boolean; keys: boolean; values: boolean; }; at(index: number): number | undefined; } | { query: ", + "FilterMetaParams", " | undefined; alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type: \"range\"; key?: string | undefined; params?: (", "FilterMetaParams", " & ", diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 56f69b7a09446..9dac7b564cfc4 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-query'] --- import kbnEsQueryObj from './kbn_es_query.devdocs.json'; diff --git a/api_docs/kbn_es_types.mdx b/api_docs/kbn_es_types.mdx index 4de4378862f0f..9d1a75a856f82 100644 --- a/api_docs/kbn_es_types.mdx +++ b/api_docs/kbn_es_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-es-types title: "@kbn/es-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/es-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/es-types'] --- import kbnEsTypesObj from './kbn_es_types.devdocs.json'; diff --git a/api_docs/kbn_eslint_plugin_imports.mdx b/api_docs/kbn_eslint_plugin_imports.mdx index 99046ee7a9a8d..cd2e3feaf7128 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/eslint-plugin-imports'] --- import kbnEslintPluginImportsObj from './kbn_eslint_plugin_imports.devdocs.json'; diff --git a/api_docs/kbn_expandable_flyout.devdocs.json b/api_docs/kbn_expandable_flyout.devdocs.json index 1590d2e5ac52d..7fcd69c291a2e 100644 --- a/api_docs/kbn_expandable_flyout.devdocs.json +++ b/api_docs/kbn_expandable_flyout.devdocs.json @@ -80,31 +80,38 @@ "\nWrap your plugin with this context for the ExpandableFlyout React component." ], "signature": [ - "({ children }: ", + "React.ForwardRefExoticComponent<", "ExpandableFlyoutProviderProps", - ") => JSX.Element" + " & React.RefAttributes<", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutApi", + "text": "ExpandableFlyoutApi" + }, + ">>" ], "path": "packages/kbn-expandable-flyout/src/context.tsx", "deprecated": false, "trackAdoption": false, + "returnComment": [], "children": [ { "parentPluginId": "@kbn/expandable-flyout", "id": "def-common.ExpandableFlyoutProvider.$1", - "type": "Object", + "type": "Uncategorized", "tags": [], - "label": "{ children }", + "label": "props", "description": [], "signature": [ - "ExpandableFlyoutProviderProps" + "P" ], - "path": "packages/kbn-expandable-flyout/src/context.tsx", + "path": "node_modules/@types/react/index.d.ts", "deprecated": false, - "trackAdoption": false, - "isRequired": true + "trackAdoption": false } ], - "returnComment": [], "initialIsOpen": false }, { @@ -118,7 +125,13 @@ ], "signature": [ "() => ", - "ExpandableFlyoutContext" + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutContext", + "text": "ExpandableFlyoutContext" + } ], "path": "packages/kbn-expandable-flyout/src/context.tsx", "deprecated": false, @@ -129,6 +142,389 @@ } ], "interfaces": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext", + "type": "Interface", + "tags": [], + "label": "ExpandableFlyoutContext", + "description": [], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.panels", + "type": "Object", + "tags": [], + "label": "panels", + "description": [ + "\nRight, left and preview panels" + ], + "signature": [ + "State" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout", + "type": "Function", + "tags": [], + "label": "openFlyout", + "description": [ + "\nOpen the flyout with left, right and/or preview panels" + ], + "signature": [ + "(panels: { left?: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined; right?: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined; preview?: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined; }) => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1", + "type": "Object", + "tags": [], + "label": "panels", + "description": [], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1.left", + "type": "Object", + "tags": [], + "label": "left", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1.right", + "type": "Object", + "tags": [], + "label": "right", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openFlyout.$1.preview", + "type": "Object", + "tags": [], + "label": "preview", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + " | undefined" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false + } + ] + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openRightPanel", + "type": "Function", + "tags": [], + "label": "openRightPanel", + "description": [ + "\nReplaces the current right panel with a new one" + ], + "signature": [ + "(panel: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + ") => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openRightPanel.$1", + "type": "Object", + "tags": [], + "label": "panel", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + } + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openLeftPanel", + "type": "Function", + "tags": [], + "label": "openLeftPanel", + "description": [ + "\nReplaces the current left panel with a new one" + ], + "signature": [ + "(panel: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + ") => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openLeftPanel.$1", + "type": "Object", + "tags": [], + "label": "panel", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + } + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openPreviewPanel", + "type": "Function", + "tags": [], + "label": "openPreviewPanel", + "description": [ + "\nAdd a new preview panel to the list of current preview panels" + ], + "signature": [ + "(panel: ", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + }, + ") => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.openPreviewPanel.$1", + "type": "Object", + "tags": [], + "label": "panel", + "description": [], + "signature": [ + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.FlyoutPanel", + "text": "FlyoutPanel" + } + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closeRightPanel", + "type": "Function", + "tags": [], + "label": "closeRightPanel", + "description": [ + "\nCloses right panel" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closeLeftPanel", + "type": "Function", + "tags": [], + "label": "closeLeftPanel", + "description": [ + "\nCloses left panel" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closePreviewPanel", + "type": "Function", + "tags": [], + "label": "closePreviewPanel", + "description": [ + "\nCloses all preview panels" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.previousPreviewPanel", + "type": "Function", + "tags": [], + "label": "previousPreviewPanel", + "description": [ + "\nGo back to previous preview panel" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext.closeFlyout", + "type": "Function", + "tags": [], + "label": "closeFlyout", + "description": [ + "\nClose all panels and closes flyout" + ], + "signature": [ + "() => void" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/expandable-flyout", "id": "def-common.ExpandableFlyoutProps", @@ -267,7 +663,57 @@ } ], "enums": [], - "misc": [], - "objects": [] + "misc": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutApi", + "type": "Type", + "tags": [], + "label": "ExpandableFlyoutApi", + "description": [], + "signature": [ + "Pick<", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutContext", + "text": "ExpandableFlyoutContext" + }, + ", \"openFlyout\"> & { getState: () => ", + "State", + "; }" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [ + { + "parentPluginId": "@kbn/expandable-flyout", + "id": "def-common.ExpandableFlyoutContext", + "type": "Object", + "tags": [], + "label": "ExpandableFlyoutContext", + "description": [], + "signature": [ + "React.Context<", + { + "pluginId": "@kbn/expandable-flyout", + "scope": "common", + "docId": "kibKbnExpandableFlyoutPluginApi", + "section": "def-common.ExpandableFlyoutContext", + "text": "ExpandableFlyoutContext" + }, + " | undefined>" + ], + "path": "packages/kbn-expandable-flyout/src/context.tsx", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ] } } \ No newline at end of file diff --git a/api_docs/kbn_expandable_flyout.mdx b/api_docs/kbn_expandable_flyout.mdx index 86a0743c821f5..d6f3aa5be6f6b 100644 --- a/api_docs/kbn_expandable_flyout.mdx +++ b/api_docs/kbn_expandable_flyout.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-expandable-flyout title: "@kbn/expandable-flyout" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/expandable-flyout plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/expandable-flyout'] --- import kbnExpandableFlyoutObj from './kbn_expandable_flyout.devdocs.json'; @@ -21,13 +21,19 @@ Contact [@elastic/security-threat-hunting-investigations](https://github.com/org | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 13 | 0 | 4 | 3 | +| 33 | 0 | 13 | 3 | ## Common +### Objects + + ### Functions ### Interfaces +### Consts, variables and types + + diff --git a/api_docs/kbn_field_types.mdx b/api_docs/kbn_field_types.mdx index d0b208fb97a8e..4d30f7635f93f 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: 2023-04-21 +date: 2023-04-24 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 46afc78d5608e..602d4b76e4b69 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: 2023-04-21 +date: 2023-04-24 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_ftr_common_functional_services.mdx b/api_docs/kbn_ftr_common_functional_services.mdx index 7ae2b45126f93..512b1b3bf1c1c 100644 --- a/api_docs/kbn_ftr_common_functional_services.mdx +++ b/api_docs/kbn_ftr_common_functional_services.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ftr-common-functional-services title: "@kbn/ftr-common-functional-services" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ftr-common-functional-services plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ftr-common-functional-services'] --- import kbnFtrCommonFunctionalServicesObj from './kbn_ftr_common_functional_services.devdocs.json'; diff --git a/api_docs/kbn_generate.mdx b/api_docs/kbn_generate.mdx index 7d52e4ad2721e..c0b9ff1ab00b0 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate'] --- import kbnGenerateObj from './kbn_generate.devdocs.json'; diff --git a/api_docs/kbn_generate_csv.mdx b/api_docs/kbn_generate_csv.mdx index bf0035738a250..3927471f08514 100644 --- a/api_docs/kbn_generate_csv.mdx +++ b/api_docs/kbn_generate_csv.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv title: "@kbn/generate-csv" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv'] --- import kbnGenerateCsvObj from './kbn_generate_csv.devdocs.json'; diff --git a/api_docs/kbn_generate_csv_types.mdx b/api_docs/kbn_generate_csv_types.mdx index 62ba0711eb511..0e35a01c9f842 100644 --- a/api_docs/kbn_generate_csv_types.mdx +++ b/api_docs/kbn_generate_csv_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-generate-csv-types title: "@kbn/generate-csv-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/generate-csv-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/generate-csv-types'] --- import kbnGenerateCsvTypesObj from './kbn_generate_csv_types.devdocs.json'; diff --git a/api_docs/kbn_guided_onboarding.devdocs.json b/api_docs/kbn_guided_onboarding.devdocs.json index ec142f58f5b6c..80777a0b54764 100644 --- a/api_docs/kbn_guided_onboarding.devdocs.json +++ b/api_docs/kbn_guided_onboarding.devdocs.json @@ -271,6 +271,27 @@ "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/guided-onboarding", + "id": "def-common.GuideState.params", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "signature": [ + { + "pluginId": "@kbn/guided-onboarding", + "scope": "common", + "docId": "kibKbnGuidedOnboardingPluginApi", + "section": "def-common.GuideParams", + "text": "GuideParams" + }, + " | undefined" + ], + "path": "packages/kbn-guided-onboarding/src/types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -294,7 +315,7 @@ "label": "id", "description": [], "signature": [ - "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\"" + "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\" | \"step4\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, @@ -336,7 +357,7 @@ "label": "id", "description": [], "signature": [ - "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\"" + "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\" | \"step4\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, @@ -558,6 +579,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/guided-onboarding", + "id": "def-common.GuideParams", + "type": "Type", + "tags": [], + "label": "GuideParams", + "description": [], + "signature": [ + "{ [x: string]: string; }" + ], + "path": "packages/kbn-guided-onboarding/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/guided-onboarding", "id": "def-common.GuideStatus", @@ -583,7 +619,7 @@ "label": "GuideStepIds", "description": [], "signature": [ - "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\"" + "\"rules\" | \"add_data\" | \"view_dashboard\" | \"tour_observability\" | \"alertsCases\" | \"search_experience\" | \"step1\" | \"step2\" | \"step3\" | \"step4\"" ], "path": "packages/kbn-guided-onboarding/src/types.ts", "deprecated": false, @@ -757,7 +793,7 @@ "label": "steps", "description": [], "signature": [ - "({ id: \"step1\"; title: string; descriptionList: string[]; location: { appID: string; path: string; }; integration: string; } | { id: \"step2\"; title: string; descriptionList: (string | { descriptionText: string; linkText: string; linkUrl: string; isLinkExternal: true; })[]; location: { appID: string; path: string; }; manualCompletion: { title: string; description: string; readyToCompleteOnNavigation: true; }; } | { id: \"step3\"; title: string; description: string; manualCompletion: { title: string; description: string; }; location: { appID: string; path: string; }; })[]" + "({ id: \"step1\"; title: string; descriptionList: string[]; location: { appID: string; path: string; }; integration: string; } | { id: \"step2\"; title: string; descriptionList: (string | { descriptionText: string; linkText: string; linkUrl: string; isLinkExternal: true; })[]; location: { appID: string; path: string; }; manualCompletion: { title: string; description: string; readyToCompleteOnNavigation: true; }; } | { id: \"step3\"; title: string; description: string; manualCompletion: { title: string; description: string; }; location: { appID: string; path: string; }; } | { id: \"step4\"; title: string; description: string; location: { appID: string; path: string; }; })[]" ], "path": "packages/kbn-guided-onboarding/src/common/test_guide_config.ts", "deprecated": false, diff --git a/api_docs/kbn_guided_onboarding.mdx b/api_docs/kbn_guided_onboarding.mdx index dd29963efcb66..fa3960f635cfa 100644 --- a/api_docs/kbn_guided_onboarding.mdx +++ b/api_docs/kbn_guided_onboarding.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-guided-onboarding title: "@kbn/guided-onboarding" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/guided-onboarding plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/guided-onboarding'] --- import kbnGuidedOnboardingObj from './kbn_guided_onboarding.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/pla | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 52 | 0 | 50 | 3 | +| 54 | 0 | 52 | 3 | ## Common diff --git a/api_docs/kbn_handlebars.mdx b/api_docs/kbn_handlebars.mdx index 7f02b89475b3e..52466ad20426f 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: 2023-04-21 +date: 2023-04-24 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 4955b1030ea07..cd8245459b82a 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/hapi-mocks'] --- import kbnHapiMocksObj from './kbn_hapi_mocks.devdocs.json'; diff --git a/api_docs/kbn_health_gateway_server.mdx b/api_docs/kbn_health_gateway_server.mdx index 26528fbcb1d05..f6df58ece9340 100644 --- a/api_docs/kbn_health_gateway_server.mdx +++ b/api_docs/kbn_health_gateway_server.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-health-gateway-server title: "@kbn/health-gateway-server" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/health-gateway-server plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/health-gateway-server'] --- import kbnHealthGatewayServerObj from './kbn_health_gateway_server.devdocs.json'; diff --git a/api_docs/kbn_home_sample_data_card.mdx b/api_docs/kbn_home_sample_data_card.mdx index ca6f2a8e2ad02..65d82c8aa5eb7 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: 2023-04-21 +date: 2023-04-24 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 ade2366ab0e33..139ab20896b0d 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: 2023-04-21 +date: 2023-04-24 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 0a6a407c727d5..c5d491f697f42 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n'] --- import kbnI18nObj from './kbn_i18n.devdocs.json'; diff --git a/api_docs/kbn_i18n_react.mdx b/api_docs/kbn_i18n_react.mdx index 7bf9b5d9dd0c8..50c599f41ff0c 100644 --- a/api_docs/kbn_i18n_react.mdx +++ b/api_docs/kbn_i18n_react.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-i18n-react title: "@kbn/i18n-react" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/i18n-react plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/i18n-react'] --- import kbnI18nReactObj from './kbn_i18n_react.devdocs.json'; diff --git a/api_docs/kbn_import_resolver.mdx b/api_docs/kbn_import_resolver.mdx index fd1da1af10952..d6377597c1189 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: 2023-04-21 +date: 2023-04-24 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 3bfb04e709422..4d82a84859ef6 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: 2023-04-21 +date: 2023-04-24 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 503dbab8e3754..d739403a3224c 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: 2023-04-21 +date: 2023-04-24 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 dea3262170428..479812a7adc8e 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/jest-serializers'] --- import kbnJestSerializersObj from './kbn_jest_serializers.devdocs.json'; diff --git a/api_docs/kbn_journeys.mdx b/api_docs/kbn_journeys.mdx index aa82431ea0e3f..66f07ded66198 100644 --- a/api_docs/kbn_journeys.mdx +++ b/api_docs/kbn_journeys.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-journeys title: "@kbn/journeys" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/journeys plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/journeys'] --- import kbnJourneysObj from './kbn_journeys.devdocs.json'; diff --git a/api_docs/kbn_json_ast.mdx b/api_docs/kbn_json_ast.mdx index cb2330485201b..9ba905d696133 100644 --- a/api_docs/kbn_json_ast.mdx +++ b/api_docs/kbn_json_ast.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-json-ast title: "@kbn/json-ast" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/json-ast plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/json-ast'] --- import kbnJsonAstObj from './kbn_json_ast.devdocs.json'; diff --git a/api_docs/kbn_kibana_manifest_schema.mdx b/api_docs/kbn_kibana_manifest_schema.mdx index 7954d741bfcb3..9b3835108da87 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/kibana-manifest-schema'] --- import kbnKibanaManifestSchemaObj from './kbn_kibana_manifest_schema.devdocs.json'; diff --git a/api_docs/kbn_language_documentation_popover.mdx b/api_docs/kbn_language_documentation_popover.mdx index 7ceb5757539a4..34e8d2903cb26 100644 --- a/api_docs/kbn_language_documentation_popover.mdx +++ b/api_docs/kbn_language_documentation_popover.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-language-documentation-popover title: "@kbn/language-documentation-popover" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/language-documentation-popover plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/language-documentation-popover'] --- import kbnLanguageDocumentationPopoverObj from './kbn_language_documentation_popover.devdocs.json'; diff --git a/api_docs/kbn_logging.mdx b/api_docs/kbn_logging.mdx index 81a30f0763d92..6c2e32868b8c7 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: 2023-04-21 +date: 2023-04-24 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 8cd4373bdcf17..c5fa54588432d 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: 2023-04-21 +date: 2023-04-24 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 f660720b2133c..a0492d6bad71a 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: 2023-04-21 +date: 2023-04-24 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 05c475dfc93c2..2cfeb2a59556c 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: 2023-04-21 +date: 2023-04-24 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 d82c025dbab1e..0faf79405aec0 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: 2023-04-21 +date: 2023-04-24 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_date_picker.mdx b/api_docs/kbn_ml_date_picker.mdx index f92ec1a980b78..6d638b542f37f 100644 --- a/api_docs/kbn_ml_date_picker.mdx +++ b/api_docs/kbn_ml_date_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-date-picker title: "@kbn/ml-date-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-date-picker plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-date-picker'] --- import kbnMlDatePickerObj from './kbn_ml_date_picker.devdocs.json'; diff --git a/api_docs/kbn_ml_error_utils.devdocs.json b/api_docs/kbn_ml_error_utils.devdocs.json new file mode 100644 index 0000000000000..8872d1df30f9a --- /dev/null +++ b/api_docs/kbn_ml_error_utils.devdocs.json @@ -0,0 +1,800 @@ +{ + "id": "@kbn/ml-error-utils", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure", + "type": "Class", + "tags": [ + "export", + "class", + "typedef", + "extends" + ], + "label": "MLRequestFailure", + "description": [ + "\nML Request Failure\n" + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLRequestFailure", + "text": "MLRequestFailure" + }, + " extends Error" + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure.Unnamed", + "type": "Function", + "tags": [ + "constructor" + ], + "label": "Constructor", + "description": [ + "\nCreates an instance of MLRequestFailure.\n" + ], + "signature": [ + "any" + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure.Unnamed.$1", + "type": "Object", + "tags": [], + "label": "error", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLErrorObject", + "text": "MLErrorObject" + } + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLRequestFailure.Unnamed.$2", + "type": "CompoundType", + "tags": [], + "label": "resp", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + } + ], + "path": "x-pack/packages/ml/error_utils/src/request_error.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + } + ], + "functions": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorMessage", + "type": "Function", + "tags": [], + "label": "extractErrorMessage", + "description": [ + "\nExtract only the error message within the response error\ncoming from Kibana, Elasticsearch, and our own ML messages.\n" + ], + "signature": [ + "(error: ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + }, + ") => string" + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorMessage.$1", + "type": "CompoundType", + "tags": [], + "label": "error", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + } + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorProperties", + "type": "Function", + "tags": [], + "label": "extractErrorProperties", + "description": [ + "\nExtract properties of the error object from within the response error\ncoming from Kibana, Elasticsearch, and our own ML messages.\n" + ], + "signature": [ + "(error: ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + }, + ") => ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLErrorObject", + "text": "MLErrorObject" + } + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.extractErrorProperties.$1", + "type": "CompoundType", + "tags": [], + "label": "error", + "description": [], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorType", + "text": "ErrorType" + } + ], + "path": "x-pack/packages/ml/error_utils/src/process_errors.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isBoomError", + "type": "Function", + "tags": [ + "export" + ], + "label": "isBoomError", + "description": [ + "\nType guard to check if error is of type Boom." + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isBoomError.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isErrorString", + "type": "Function", + "tags": [ + "export" + ], + "label": "isErrorString", + "description": [ + "\nType guard to check if error is a string." + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isErrorString.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isEsErrorBody", + "type": "Function", + "tags": [ + "export" + ], + "label": "isEsErrorBody", + "description": [ + "\nType guard to check if error is of type EsErrorBody" + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isEsErrorBody.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isMLResponseError", + "type": "Function", + "tags": [ + "export" + ], + "label": "isMLResponseError", + "description": [ + "\nType guard to check if error is of type MLResponseError." + ], + "signature": [ + "(error: unknown) => boolean" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.isMLResponseError.$1", + "type": "Unknown", + "tags": [], + "label": "error", + "description": [], + "signature": [ + "unknown" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.ErrorMessage", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef" + ], + "label": "ErrorMessage", + "description": [ + "\nInterface holding error message" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.ErrorMessage.message", + "type": "string", + "tags": [ + "type" + ], + "label": "message", + "description": [ + "\nmessage" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef" + ], + "label": "MLErrorObject", + "description": [ + "\nML Error Object" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.causedBy", + "type": "string", + "tags": [ + "type" + ], + "label": "causedBy", + "description": [ + "\nOptional causedBy" + ], + "signature": [ + "string | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.message", + "type": "string", + "tags": [ + "type" + ], + "label": "message", + "description": [ + "\nmessage" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.statusCode", + "type": "number", + "tags": [ + "type" + ], + "label": "statusCode", + "description": [ + "\nOptional statusCode" + ], + "signature": [ + "number | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLErrorObject.fullError", + "type": "Object", + "tags": [ + "type" + ], + "label": "fullError", + "description": [ + "\nOptional fullError" + ], + "signature": [ + "ErrorResponseBase", + " | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLHttpFetchErrorBase", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef", + "template", + "extends" + ], + "label": "MLHttpFetchErrorBase", + "description": [ + "\nMLHttpFetchErrorBase" + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLHttpFetchErrorBase", + "text": "MLHttpFetchErrorBase" + }, + " extends ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.IHttpFetchError", + "text": "IHttpFetchError" + }, + "" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLHttpFetchErrorBase.body", + "type": "Uncategorized", + "tags": [ + "type" + ], + "label": "body", + "description": [ + "\nbody" + ], + "signature": [ + "T" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError", + "type": "Interface", + "tags": [ + "export", + "interface", + "typedef" + ], + "label": "MLResponseError", + "description": [ + "\nML Response error" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.statusCode", + "type": "number", + "tags": [ + "type" + ], + "label": "statusCode", + "description": [ + "\nstatusCode" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.error", + "type": "string", + "tags": [ + "type" + ], + "label": "error", + "description": [ + "\nerror" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.message", + "type": "string", + "tags": [ + "type" + ], + "label": "message", + "description": [ + "\nmessage" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLResponseError.attributes", + "type": "Object", + "tags": [ + "type" + ], + "label": "attributes", + "description": [ + "\nOptional attributes" + ], + "signature": [ + "{ body: ", + "ErrorResponseBase", + "; } | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.QueryErrorMessage", + "type": "Interface", + "tags": [], + "label": "QueryErrorMessage", + "description": [ + "\nTo be used for client side errors related to search query bars." + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.QueryErrorMessage", + "text": "QueryErrorMessage" + }, + " extends ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.ErrorMessage", + "text": "ErrorMessage" + } + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.QueryErrorMessage.query", + "type": "string", + "tags": [ + "type" + ], + "label": "query", + "description": [ + "\nquery" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.ErrorType", + "type": "Type", + "tags": [ + "export", + "typedef" + ], + "label": "ErrorType", + "description": [ + "\nUnion type of error types" + ], + "signature": [ + "string | ", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLHttpFetchError", + "text": "MLHttpFetchError" + }, + " | ", + "ErrorResponseBase", + " | ", + "Boom", + " | undefined" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.EsErrorBody", + "type": "Type", + "tags": [ + "typedef" + ], + "label": "EsErrorBody", + "description": [ + "\nShort hand type of estypes.ErrorResponseBase." + ], + "signature": [ + "ErrorResponseBase" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.EsErrorRootCause", + "type": "Type", + "tags": [ + "typedef" + ], + "label": "EsErrorRootCause", + "description": [ + "\nShort hand type of estypes.ErrorCause." + ], + "signature": [ + "ErrorCauseKeys", + " & { [property: string]: any; }" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/ml-error-utils", + "id": "def-common.MLHttpFetchError", + "type": "Type", + "tags": [ + "export", + "typedef" + ], + "label": "MLHttpFetchError", + "description": [ + "\nMLHttpFetchError" + ], + "signature": [ + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLHttpFetchErrorBase", + "text": "MLHttpFetchErrorBase" + }, + "<", + { + "pluginId": "@kbn/ml-error-utils", + "scope": "common", + "docId": "kibKbnMlErrorUtilsPluginApi", + "section": "def-common.MLResponseError", + "text": "MLResponseError" + }, + ">" + ], + "path": "x-pack/packages/ml/error_utils/src/types.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/kbn_ml_error_utils.mdx b/api_docs/kbn_ml_error_utils.mdx new file mode 100644 index 0000000000000..39fbc8c7d9cc2 --- /dev/null +++ b/api_docs/kbn_ml_error_utils.mdx @@ -0,0 +1,39 @@ +--- +#### +#### 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: kibKbnMlErrorUtilsPluginApi +slug: /kibana-dev-docs/api/kbn-ml-error-utils +title: "@kbn/ml-error-utils" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/ml-error-utils plugin +date: 2023-04-24 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-error-utils'] +--- +import kbnMlErrorUtilsObj from './kbn_ml_error_utils.devdocs.json'; + + + +Contact [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 36 | 0 | 8 | 0 | + +## Common + +### Functions + + +### Classes + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/kbn_ml_is_defined.mdx b/api_docs/kbn_ml_is_defined.mdx index 9d000095378bd..22473afe43b10 100644 --- a/api_docs/kbn_ml_is_defined.mdx +++ b/api_docs/kbn_ml_is_defined.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-is-defined title: "@kbn/ml-is-defined" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-is-defined plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-is-defined'] --- import kbnMlIsDefinedObj from './kbn_ml_is_defined.devdocs.json'; diff --git a/api_docs/kbn_ml_is_populated_object.mdx b/api_docs/kbn_ml_is_populated_object.mdx index 40ce5731de31e..6418b8eaa65f6 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: 2023-04-21 +date: 2023-04-24 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_local_storage.mdx b/api_docs/kbn_ml_local_storage.mdx index f14deff209d77..b6ab8338b1be1 100644 --- a/api_docs/kbn_ml_local_storage.mdx +++ b/api_docs/kbn_ml_local_storage.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-local-storage title: "@kbn/ml-local-storage" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-local-storage plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-local-storage'] --- import kbnMlLocalStorageObj from './kbn_ml_local_storage.devdocs.json'; diff --git a/api_docs/kbn_ml_nested_property.mdx b/api_docs/kbn_ml_nested_property.mdx index ef15635229b53..20e06ed847ebb 100644 --- a/api_docs/kbn_ml_nested_property.mdx +++ b/api_docs/kbn_ml_nested_property.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-nested-property title: "@kbn/ml-nested-property" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-nested-property plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-nested-property'] --- import kbnMlNestedPropertyObj from './kbn_ml_nested_property.devdocs.json'; diff --git a/api_docs/kbn_ml_number_utils.mdx b/api_docs/kbn_ml_number_utils.mdx index 05c5432c4f57f..e17712d9bddd9 100644 --- a/api_docs/kbn_ml_number_utils.mdx +++ b/api_docs/kbn_ml_number_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-number-utils title: "@kbn/ml-number-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-number-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-number-utils'] --- import kbnMlNumberUtilsObj from './kbn_ml_number_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_query_utils.mdx b/api_docs/kbn_ml_query_utils.mdx index 8327a7ed0b4d5..ceef878888760 100644 --- a/api_docs/kbn_ml_query_utils.mdx +++ b/api_docs/kbn_ml_query_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-query-utils title: "@kbn/ml-query-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-query-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-query-utils'] --- import kbnMlQueryUtilsObj from './kbn_ml_query_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_random_sampler_utils.mdx b/api_docs/kbn_ml_random_sampler_utils.mdx index 4998fe175ef71..e065a2d921874 100644 --- a/api_docs/kbn_ml_random_sampler_utils.mdx +++ b/api_docs/kbn_ml_random_sampler_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-random-sampler-utils title: "@kbn/ml-random-sampler-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-random-sampler-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-random-sampler-utils'] --- import kbnMlRandomSamplerUtilsObj from './kbn_ml_random_sampler_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_route_utils.mdx b/api_docs/kbn_ml_route_utils.mdx index 8e1d50e0f9364..619f44aa95904 100644 --- a/api_docs/kbn_ml_route_utils.mdx +++ b/api_docs/kbn_ml_route_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-route-utils title: "@kbn/ml-route-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-route-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-route-utils'] --- import kbnMlRouteUtilsObj from './kbn_ml_route_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_string_hash.mdx b/api_docs/kbn_ml_string_hash.mdx index f18f1327cb256..c338805fe37b6 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-string-hash'] --- import kbnMlStringHashObj from './kbn_ml_string_hash.devdocs.json'; diff --git a/api_docs/kbn_ml_trained_models_utils.mdx b/api_docs/kbn_ml_trained_models_utils.mdx index 43aeec4bf32ca..77df8b9fc96a9 100644 --- a/api_docs/kbn_ml_trained_models_utils.mdx +++ b/api_docs/kbn_ml_trained_models_utils.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-trained-models-utils title: "@kbn/ml-trained-models-utils" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-trained-models-utils plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-trained-models-utils'] --- import kbnMlTrainedModelsUtilsObj from './kbn_ml_trained_models_utils.devdocs.json'; diff --git a/api_docs/kbn_ml_url_state.mdx b/api_docs/kbn_ml_url_state.mdx index 390009d4685e5..271d8d798bb8b 100644 --- a/api_docs/kbn_ml_url_state.mdx +++ b/api_docs/kbn_ml_url_state.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ml-url-state title: "@kbn/ml-url-state" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ml-url-state plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ml-url-state'] --- import kbnMlUrlStateObj from './kbn_ml_url_state.devdocs.json'; diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 605d07152c5b0..0d286dc40dd46 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/monaco'] --- import kbnMonacoObj from './kbn_monaco.devdocs.json'; diff --git a/api_docs/kbn_object_versioning.devdocs.json b/api_docs/kbn_object_versioning.devdocs.json index bb1210067600d..551e1feda1d0b 100644 --- a/api_docs/kbn_object_versioning.devdocs.json +++ b/api_docs/kbn_object_versioning.devdocs.json @@ -768,6 +768,28 @@ "path": "packages/kbn-object-versioning/lib/content_management_types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/object-versioning", + "id": "def-common.ServicesDefinition.mSearch", + "type": "Object", + "tags": [], + "label": "mSearch", + "description": [], + "signature": [ + "{ out?: { result?: ", + { + "pluginId": "@kbn/object-versioning", + "scope": "common", + "docId": "kibKbnObjectVersioningPluginApi", + "section": "def-common.VersionableObject", + "text": "VersionableObject" + }, + " | undefined; } | undefined; } | undefined" + ], + "path": "packages/kbn-object-versioning/lib/content_management_types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -978,6 +1000,28 @@ "path": "packages/kbn-object-versioning/lib/content_management_types.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "@kbn/object-versioning", + "id": "def-common.ServiceTransforms.mSearch", + "type": "Object", + "tags": [], + "label": "mSearch", + "description": [], + "signature": [ + "{ out: { result: ", + { + "pluginId": "@kbn/object-versioning", + "scope": "common", + "docId": "kibKbnObjectVersioningPluginApi", + "section": "def-common.ObjectTransforms", + "text": "ObjectTransforms" + }, + "; }; }" + ], + "path": "packages/kbn-object-versioning/lib/content_management_types.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/kbn_object_versioning.mdx b/api_docs/kbn_object_versioning.mdx index 8d3135bdd7f76..bf8dc9c9b2edb 100644 --- a/api_docs/kbn_object_versioning.mdx +++ b/api_docs/kbn_object_versioning.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-object-versioning title: "@kbn/object-versioning" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/object-versioning plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/object-versioning'] --- import kbnObjectVersioningObj from './kbn_object_versioning.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sh | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 53 | 1 | 48 | 0 | +| 55 | 1 | 50 | 0 | ## Common diff --git a/api_docs/kbn_observability_alert_details.mdx b/api_docs/kbn_observability_alert_details.mdx index 48886f59e6160..109f0b15690e7 100644 --- a/api_docs/kbn_observability_alert_details.mdx +++ b/api_docs/kbn_observability_alert_details.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-observability-alert-details title: "@kbn/observability-alert-details" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/observability-alert-details plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/observability-alert-details'] --- import kbnObservabilityAlertDetailsObj from './kbn_observability_alert_details.devdocs.json'; diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index 58204fde5d980..2be790b6675bf 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: 2023-04-21 +date: 2023-04-24 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 ac5cab95f43ec..c03883615b27e 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/optimizer-webpack-helpers'] --- import kbnOptimizerWebpackHelpersObj from './kbn_optimizer_webpack_helpers.devdocs.json'; diff --git a/api_docs/kbn_osquery_io_ts_types.mdx b/api_docs/kbn_osquery_io_ts_types.mdx index d351b9e974a46..39eafb2553e2a 100644 --- a/api_docs/kbn_osquery_io_ts_types.mdx +++ b/api_docs/kbn_osquery_io_ts_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-osquery-io-ts-types title: "@kbn/osquery-io-ts-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/osquery-io-ts-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/osquery-io-ts-types'] --- import kbnOsqueryIoTsTypesObj from './kbn_osquery_io_ts_types.devdocs.json'; diff --git a/api_docs/kbn_performance_testing_dataset_extractor.mdx b/api_docs/kbn_performance_testing_dataset_extractor.mdx index 820b381b56e3b..fe2a28edfb759 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: 2023-04-21 +date: 2023-04-24 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 4f63a60e98d21..dfafc2995046a 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: 2023-04-21 +date: 2023-04-24 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 481b254612404..90bfd0727dc86 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: 2023-04-21 +date: 2023-04-24 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 5d88f2bb7cd87..894f6e1897cb7 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/react-field'] --- import kbnReactFieldObj from './kbn_react_field.devdocs.json'; diff --git a/api_docs/kbn_repo_file_maps.mdx b/api_docs/kbn_repo_file_maps.mdx index fb0c69e268327..9baafe7d47198 100644 --- a/api_docs/kbn_repo_file_maps.mdx +++ b/api_docs/kbn_repo_file_maps.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-file-maps title: "@kbn/repo-file-maps" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-file-maps plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-file-maps'] --- import kbnRepoFileMapsObj from './kbn_repo_file_maps.devdocs.json'; diff --git a/api_docs/kbn_repo_linter.mdx b/api_docs/kbn_repo_linter.mdx index fa756525922ae..0dcb15d07b2b0 100644 --- a/api_docs/kbn_repo_linter.mdx +++ b/api_docs/kbn_repo_linter.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-linter title: "@kbn/repo-linter" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-linter plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-linter'] --- import kbnRepoLinterObj from './kbn_repo_linter.devdocs.json'; diff --git a/api_docs/kbn_repo_path.mdx b/api_docs/kbn_repo_path.mdx index dc04d8563bc52..44df1cb8c7ba8 100644 --- a/api_docs/kbn_repo_path.mdx +++ b/api_docs/kbn_repo_path.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-repo-path title: "@kbn/repo-path" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/repo-path plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-path'] --- import kbnRepoPathObj from './kbn_repo_path.devdocs.json'; diff --git a/api_docs/kbn_repo_source_classifier.mdx b/api_docs/kbn_repo_source_classifier.mdx index af0847941070f..3725fe5986da2 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/repo-source-classifier'] --- import kbnRepoSourceClassifierObj from './kbn_repo_source_classifier.devdocs.json'; diff --git a/api_docs/kbn_reporting_common.mdx b/api_docs/kbn_reporting_common.mdx index 52ff2a0ac41b5..660528756fcdf 100644 --- a/api_docs/kbn_reporting_common.mdx +++ b/api_docs/kbn_reporting_common.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-reporting-common title: "@kbn/reporting-common" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/reporting-common plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/reporting-common'] --- import kbnReportingCommonObj from './kbn_reporting_common.devdocs.json'; diff --git a/api_docs/kbn_rison.mdx b/api_docs/kbn_rison.mdx index 5f654ffa57ef8..ee1fd9df0086e 100644 --- a/api_docs/kbn_rison.mdx +++ b/api_docs/kbn_rison.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-rison title: "@kbn/rison" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/rison plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rison'] --- import kbnRisonObj from './kbn_rison.devdocs.json'; diff --git a/api_docs/kbn_rule_data_utils.mdx b/api_docs/kbn_rule_data_utils.mdx index 16eb8b268e12a..56b40b98c6111 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/rule-data-utils'] --- import kbnRuleDataUtilsObj from './kbn_rule_data_utils.devdocs.json'; diff --git a/api_docs/kbn_saved_objects_settings.mdx b/api_docs/kbn_saved_objects_settings.mdx index 2d5a7a078d24c..ac3326c810bdf 100644 --- a/api_docs/kbn_saved_objects_settings.mdx +++ b/api_docs/kbn_saved_objects_settings.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-saved-objects-settings title: "@kbn/saved-objects-settings" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/saved-objects-settings plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/saved-objects-settings'] --- import kbnSavedObjectsSettingsObj from './kbn_saved_objects_settings.devdocs.json'; diff --git a/api_docs/kbn_security_solution_side_nav.mdx b/api_docs/kbn_security_solution_side_nav.mdx index 476ecb69cdbcc..dbd27615a2886 100644 --- a/api_docs/kbn_security_solution_side_nav.mdx +++ b/api_docs/kbn_security_solution_side_nav.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-side-nav title: "@kbn/security-solution-side-nav" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-side-nav plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-side-nav'] --- import kbnSecuritySolutionSideNavObj from './kbn_security_solution_side_nav.devdocs.json'; diff --git a/api_docs/kbn_security_solution_storybook_config.mdx b/api_docs/kbn_security_solution_storybook_config.mdx index 5bec888bb4e5a..31bc6c9d9cbc9 100644 --- a/api_docs/kbn_security_solution_storybook_config.mdx +++ b/api_docs/kbn_security_solution_storybook_config.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-security-solution-storybook-config title: "@kbn/security-solution-storybook-config" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/security-solution-storybook-config plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/security-solution-storybook-config'] --- import kbnSecuritySolutionStorybookConfigObj from './kbn_security_solution_storybook_config.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_autocomplete.mdx b/api_docs/kbn_securitysolution_autocomplete.mdx index 7d0c70dbb7b39..f51ac2903abc5 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-autocomplete'] --- import kbnSecuritysolutionAutocompleteObj from './kbn_securitysolution_autocomplete.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_data_table.mdx b/api_docs/kbn_securitysolution_data_table.mdx index 1c5b3548f92c4..cbb7736cb66be 100644 --- a/api_docs/kbn_securitysolution_data_table.mdx +++ b/api_docs/kbn_securitysolution_data_table.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-data-table title: "@kbn/securitysolution-data-table" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-data-table plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-data-table'] --- import kbnSecuritysolutionDataTableObj from './kbn_securitysolution_data_table.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_ecs.mdx b/api_docs/kbn_securitysolution_ecs.mdx index 085a9a78a69cf..2d549e62753a9 100644 --- a/api_docs/kbn_securitysolution_ecs.mdx +++ b/api_docs/kbn_securitysolution_ecs.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-ecs title: "@kbn/securitysolution-ecs" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-ecs plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-ecs'] --- import kbnSecuritysolutionEcsObj from './kbn_securitysolution_ecs.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_es_utils.mdx b/api_docs/kbn_securitysolution_es_utils.mdx index 1df63d4877934..966df1d43affc 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: 2023-04-21 +date: 2023-04-24 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_exception_list_components.mdx b/api_docs/kbn_securitysolution_exception_list_components.mdx index 35453fb0b1a31..ef163cd54d784 100644 --- a/api_docs/kbn_securitysolution_exception_list_components.mdx +++ b/api_docs/kbn_securitysolution_exception_list_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-exception-list-components title: "@kbn/securitysolution-exception-list-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-exception-list-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-exception-list-components'] --- import kbnSecuritysolutionExceptionListComponentsObj from './kbn_securitysolution_exception_list_components.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_grouping.mdx b/api_docs/kbn_securitysolution_grouping.mdx index 7276dcef35bd3..548914f17feb8 100644 --- a/api_docs/kbn_securitysolution_grouping.mdx +++ b/api_docs/kbn_securitysolution_grouping.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-securitysolution-grouping title: "@kbn/securitysolution-grouping" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/securitysolution-grouping plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-grouping'] --- import kbnSecuritysolutionGroupingObj from './kbn_securitysolution_grouping.devdocs.json'; diff --git a/api_docs/kbn_securitysolution_hook_utils.mdx b/api_docs/kbn_securitysolution_hook_utils.mdx index a7cb93e0ca41c..012e37a5de10c 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: 2023-04-21 +date: 2023-04-24 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.devdocs.json b/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json index 59129f93d4ed5..9450fd73b6671 100644 --- a/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json +++ b/api_docs/kbn_securitysolution_io_ts_alerting_types.devdocs.json @@ -303,7 +303,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; }" + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; }" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -326,7 +326,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[]" + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[]" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -349,7 +349,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alertsFilter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[]" + "; } & { uuid?: string | undefined; alertsFilter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[]" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -372,13 +372,30 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alertsFilter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; }" + "; } & { uuid?: string | undefined; alertsFilter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; }" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionFrequency", + "type": "Type", + "tags": [], + "label": "RuleActionFrequency", + "description": [ + "\nThe action frequency defines when the action runs (for example, only on rule execution or at specific time intervals)." + ], + "signature": [ + "{ summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; }" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionGroup", @@ -409,6 +426,23 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionNotifyWhen", + "type": "Type", + "tags": [], + "label": "RuleActionNotifyWhen", + "description": [ + "\nThe condition for throttling the notification: `onActionGroupChange`, `onActiveAlert`, or `onThrottleInterval`" + ], + "signature": [ + "\"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionParams", @@ -434,6 +468,23 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionSummary", + "type": "Type", + "tags": [], + "label": "RuleActionSummary", + "description": [ + "\nAction summary indicates whether we will send a summary notification about all the generate alerts or notification per individual alert" + ], + "signature": [ + "boolean" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionThrottle", @@ -1013,7 +1064,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[], ({ group: string; id: string; action_type_id: string; params: ", + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[], ({ group: string; id: string; action_type_id: string; params: ", { "pluginId": "@kbn/securitysolution-io-ts-alerting-types", "scope": "common", @@ -1021,7 +1072,7 @@ "section": "def-common.SavedObjectAttributes", "text": "SavedObjectAttributes" }, - "; } & { uuid?: string | undefined; alerts_filter?: { query: ({ kql: string; } & { dsl?: string | undefined; }) | null; timeframe: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | null; } | undefined; })[] | undefined, unknown>" + "; } & { uuid?: string | undefined; alerts_filter?: { query?: ({ kql: string; filters: ({ meta: { alias?: string | null | undefined; disabled?: boolean | undefined; negate?: boolean | undefined; controlledBy?: string | undefined; group?: string | undefined; index?: string | undefined; isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; value?: string | undefined; }; } & { $state?: { store: any; } | undefined; query?: { [x: string]: any; } | undefined; })[]; } & { dsl?: string | undefined; }) | undefined; timeframe?: { timezone: string; days: (2 | 7 | 6 | 5 | 4 | 3 | 1)[]; hours: { start: string; end: string; }; } | undefined; } | undefined; frequency?: { summary: boolean; notifyWhen: \"onActionGroupChange\" | \"onActiveAlert\" | \"onThrottleInterval\"; throttle: string | null; } | undefined; })[] | undefined, unknown>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/default_actions_array/index.ts", "deprecated": false, @@ -1570,13 +1621,11 @@ "<{ uuid: ", "Type", "; alerts_filter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1585,14 +1634,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1625,7 +1720,31 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1640,13 +1759,11 @@ "label": "RuleActionAlertsFilter", "description": [], "signature": [ - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1655,14 +1772,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1695,7 +1858,7 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>" + "; }>>; }>>]>; }>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1746,13 +1909,11 @@ "<{ uuid: ", "Type", "; alerts_filter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1761,14 +1922,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1801,7 +2008,31 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1852,13 +2083,11 @@ "<{ uuid: ", "Type", "; alertsFilter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1867,14 +2096,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -1907,7 +2182,31 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, @@ -1956,13 +2255,11 @@ "<{ uuid: ", "Type", "; alertsFilter: ", - "ExactC", - "<", - "TypeC", + "PartialC", "<{ query: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "IntersectionC", "<[", @@ -1971,14 +2268,60 @@ "TypeC", "<{ kql: ", "StringC", - "; }>>, ", + "; filters: ", + "ArrayC", + "<", + "IntersectionC", + "<[", + "TypeC", + "<{ meta: ", + "PartialC", + "<{ alias: ", + "UnionC", + "<[", + "StringC", + ", ", + "NullC", + "]>; disabled: ", + "BooleanC", + "; negate: ", + "BooleanC", + "; controlledBy: ", + "StringC", + "; group: ", + "StringC", + "; index: ", + "StringC", + "; isMultiIndex: ", + "BooleanC", + "; type: ", + "StringC", + "; key: ", + "StringC", + "; params: ", + "AnyC", + "; value: ", + "StringC", + "; }>; }>, ", + "PartialC", + "<{ $state: ", + "TypeC", + "<{ store: ", + "AnyC", + "; }>; query: ", + "RecordC", + "<", + "StringC", + ", ", + "AnyC", + ">; }>]>>; }>>, ", "PartialC", "<{ dsl: ", "StringC", "; }>]>]>; timeframe: ", "UnionC", "<[", - "NullC", + "UndefinedC", ", ", "ExactC", "<", @@ -2011,13 +2354,75 @@ "StringC", "; end: ", "StringC", - "; }>>; }>>]>; }>>; }>]>>" + "; }>>; }>>]>; }>; frequency: ", + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>; }>]>>" ], "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/actions/index.ts", "deprecated": false, "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionFrequency", + "type": "Object", + "tags": [], + "label": "RuleActionFrequency", + "description": [], + "signature": [ + "TypeC", + "<{ summary: ", + "BooleanC", + "; notifyWhen: ", + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>; throttle: ", + "UnionC", + "<[", + "UnionC", + "<[", + "LiteralC", + "<\"no_actions\">, ", + "LiteralC", + "<\"rule\">, ", + "Type", + "]>, ", + "NullC", + "]>; }>" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionGroup", @@ -2048,6 +2453,28 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionNotifyWhen", + "type": "Object", + "tags": [], + "label": "RuleActionNotifyWhen", + "description": [], + "signature": [ + "UnionC", + "<[", + "LiteralC", + "<\"onActionGroupChange\">, ", + "LiteralC", + "<\"onActiveAlert\">, ", + "LiteralC", + "<\"onThrottleInterval\">]>" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionParams", @@ -2080,6 +2507,21 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", + "id": "def-common.RuleActionSummary", + "type": "Object", + "tags": [], + "label": "RuleActionSummary", + "description": [], + "signature": [ + "BooleanC" + ], + "path": "packages/kbn-securitysolution-io-ts-alerting-types/src/frequency/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-alerting-types", "id": "def-common.RuleActionThrottle", diff --git a/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx b/api_docs/kbn_securitysolution_io_ts_alerting_types.mdx index 1a5fe7a8eff63..c19977b427358 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-alerting-types'] --- import kbnSecuritysolutionIoTsAlertingTypesObj from './kbn_securitysolution_io_ts_alerting_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 141 | 0 | 122 | 0 | +| 147 | 0 | 125 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json b/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json index cbd27335f2f35..44e52b99ed031 100644 --- a/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json +++ b/api_docs/kbn_securitysolution_io_ts_list_types.devdocs.json @@ -106,7 +106,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -148,7 +154,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -204,7 +216,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -260,7 +278,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -327,7 +351,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -439,7 +469,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1015,6 +1051,102 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps", + "type": "Interface", + "tags": [], + "label": "ApiListDuplicateProps", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.ApiListDuplicateProps", + "text": "ApiListDuplicateProps" + }, + " extends Omit<", + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + }, + ", \"http\" | \"signal\">" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onError", + "type": "Function", + "tags": [], + "label": "onError", + "description": [], + "signature": [ + "(err: Error) => void" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onError.$1", + "type": "Object", + "tags": [], + "label": "err", + "description": [], + "signature": [ + "Error" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onSuccess", + "type": "Function", + "tags": [], + "label": "onSuccess", + "description": [], + "signature": [ + "(newList: { _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; }) => void" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.ApiListDuplicateProps.onSuccess.$1", + "type": "Object", + "tags": [], + "label": "newList", + "description": [], + "signature": [ + "{ _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; }" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.ApiListExportProps", @@ -1140,6 +1272,66 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps", + "type": "Interface", + "tags": [], + "label": "DuplicateExceptionListProps", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + }, + " extends BaseParams" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps.listId", + "type": "string", + "tags": [], + "label": "listId", + "description": [], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps.namespaceType", + "type": "CompoundType", + "tags": [], + "label": "namespaceType", + "description": [], + "signature": [ + "\"single\" | \"agnostic\"" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListProps.includeExpiredExceptions", + "type": "boolean", + "tags": [], + "label": "includeExpiredExceptions", + "description": [], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", + "deprecated": false, + "trackAdoption": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.ExceptionFilterResponse", @@ -1393,7 +1585,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1538,7 +1736,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1597,7 +1801,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1759,7 +1969,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1855,7 +2071,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -1911,7 +2133,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -2015,7 +2243,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -2270,7 +2504,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -2293,12 +2533,18 @@ { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.UseExceptionListsProps.notifications", - "type": "Any", + "type": "Object", "tags": [], "label": "notifications", "description": [], "signature": [ - "any" + { + "pluginId": "@kbn/core-notifications-browser", + "scope": "common", + "docId": "kibKbnCoreNotificationsBrowserPluginApi", + "section": "def-common.NotificationsStart", + "text": "NotificationsStart" + } ], "path": "packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts", "deprecated": false, @@ -3081,6 +3327,36 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListQuerySchema", + "type": "Type", + "tags": [], + "label": "DuplicateExceptionListQuerySchema", + "description": [], + "signature": [ + "{ list_id: string; namespace_type: \"single\" | \"agnostic\" | undefined; include_expired_exceptions: \"true\" | \"false\" | undefined; }" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.DuplicateExceptionListQuerySchemaDecoded", + "type": "Type", + "tags": [], + "label": "DuplicateExceptionListQuerySchemaDecoded", + "description": [], + "signature": [ + "Omit<{ list_id: string; namespace_type: \"single\" | \"agnostic\"; include_expired_exceptions: \"true\" | \"false\" | undefined; }, \"namespace_type\"> & { namespace_type: \"single\" | \"agnostic\"; }" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.EndpointEntriesArray", @@ -6256,6 +6532,34 @@ "trackAdoption": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-io-ts-list-types", + "id": "def-common.duplicateExceptionListQuerySchema", + "type": "Object", + "tags": [], + "label": "duplicateExceptionListQuerySchema", + "description": [], + "signature": [ + "ExactC", + "<", + "TypeC", + "<{ list_id: ", + "Type", + "; namespace_type: ", + "Type", + "<\"single\" | \"agnostic\", \"single\" | \"agnostic\" | undefined, unknown>; include_expired_exceptions: ", + "UnionC", + "<[", + "KeyofC", + "<{ true: null; false: null; }>, ", + "UndefinedC", + "]>; }>>" + ], + "path": "packages/kbn-securitysolution-io-ts-list-types/src/request/duplicate_exception_list_query_schema/index.ts", + "deprecated": false, + "trackAdoption": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-io-ts-list-types", "id": "def-common.endpointEntriesArray", diff --git a/api_docs/kbn_securitysolution_io_ts_list_types.mdx b/api_docs/kbn_securitysolution_io_ts_list_types.mdx index fae1f7cdd2313..61c51d7af0af1 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-io-ts-list-types'] --- import kbnSecuritysolutionIoTsListTypesObj from './kbn_securitysolution_io_ts_list_types.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 516 | 1 | 503 | 0 | +| 528 | 0 | 515 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_io_ts_types.mdx b/api_docs/kbn_securitysolution_io_ts_types.mdx index 360fd19200755..97859ef4b0ff6 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: 2023-04-21 +date: 2023-04-24 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 2b6f02cc7215b..087b27374d68b 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: 2023-04-21 +date: 2023-04-24 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.devdocs.json b/api_docs/kbn_securitysolution_list_api.devdocs.json index b4c5635b5b281..1a07636db6652 100644 --- a/api_docs/kbn_securitysolution_list_api.devdocs.json +++ b/api_docs/kbn_securitysolution_list_api.devdocs.json @@ -348,6 +348,57 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/securitysolution-list-api", + "id": "def-common.duplicateExceptionList", + "type": "Function", + "tags": [ + "throws" + ], + "label": "duplicateExceptionList", + "description": [ + "\nDuplicate an ExceptionList and its items by providing a ExceptionList list_id\n" + ], + "signature": [ + "({ http, includeExpiredExceptions, listId, namespaceType, signal, }: ", + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + }, + ") => Promise<{ _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; }>" + ], + "path": "packages/kbn-securitysolution-list-api/src/api/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-list-api", + "id": "def-common.duplicateExceptionList.$1", + "type": "Object", + "tags": [], + "label": "{\n http,\n includeExpiredExceptions,\n listId,\n namespaceType,\n signal,\n}", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.DuplicateExceptionListProps", + "text": "DuplicateExceptionListProps" + } + ], + "path": "packages/kbn-securitysolution-list-api/src/api/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/securitysolution-list-api", "id": "def-common.exportExceptionList", diff --git a/api_docs/kbn_securitysolution_list_api.mdx b/api_docs/kbn_securitysolution_list_api.mdx index 0255ba2088e5e..aadca05334b92 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-api'] --- import kbnSecuritysolutionListApiObj from './kbn_securitysolution_list_api.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 67 | 0 | 64 | 0 | +| 69 | 0 | 65 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_list_constants.mdx b/api_docs/kbn_securitysolution_list_constants.mdx index 02b65ca6fde37..2d19d6111ef53 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: 2023-04-21 +date: 2023-04-24 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.devdocs.json b/api_docs/kbn_securitysolution_list_hooks.devdocs.json index a1741a54232f0..14c2ee5a4b122 100644 --- a/api_docs/kbn_securitysolution_list_hooks.devdocs.json +++ b/api_docs/kbn_securitysolution_list_hooks.devdocs.json @@ -255,7 +255,15 @@ "label": "useApi", "description": [], "signature": [ - "(http: HttpStart) => ", + "(http: ", + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + }, + ") => ", { "pluginId": "@kbn/securitysolution-list-hooks", "scope": "common", @@ -276,7 +284,13 @@ "label": "http", "description": [], "signature": [ - "HttpStart" + { + "pluginId": "@kbn/core-http-browser", + "scope": "common", + "docId": "kibKbnCoreHttpBrowserPluginApi", + "section": "def-common.HttpSetup", + "text": "HttpSetup" + } ], "path": "packages/kbn-securitysolution-list-hooks/src/use_api/index.ts", "deprecated": false, @@ -1023,6 +1037,52 @@ ], "returnComment": [] }, + { + "parentPluginId": "@kbn/securitysolution-list-hooks", + "id": "def-common.ExceptionsApi.duplicateExceptionList", + "type": "Function", + "tags": [], + "label": "duplicateExceptionList", + "description": [], + "signature": [ + "(arg: ", + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.ApiListDuplicateProps", + "text": "ApiListDuplicateProps" + }, + ") => Promise" + ], + "path": "packages/kbn-securitysolution-list-hooks/src/use_api/index.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/securitysolution-list-hooks", + "id": "def-common.ExceptionsApi.duplicateExceptionList.$1", + "type": "Object", + "tags": [], + "label": "arg", + "description": [], + "signature": [ + { + "pluginId": "@kbn/securitysolution-io-ts-list-types", + "scope": "common", + "docId": "kibKbnSecuritysolutionIoTsListTypesPluginApi", + "section": "def-common.ApiListDuplicateProps", + "text": "ApiListDuplicateProps" + } + ], + "path": "packages/kbn-securitysolution-list-hooks/src/use_api/index.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + } + ], + "returnComment": [] + }, { "parentPluginId": "@kbn/securitysolution-list-hooks", "id": "def-common.ExceptionsApi.getExceptionItem", diff --git a/api_docs/kbn_securitysolution_list_hooks.mdx b/api_docs/kbn_securitysolution_list_hooks.mdx index 786b822ce39de..e5f63ce69a791 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/securitysolution-list-hooks'] --- import kbnSecuritysolutionListHooksObj from './kbn_securitysolution_list_hooks.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/security-solution-platform](https://github.com/orgs/elastic/te | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 60 | 0 | 47 | 0 | +| 62 | 0 | 49 | 0 | ## Common diff --git a/api_docs/kbn_securitysolution_list_utils.mdx b/api_docs/kbn_securitysolution_list_utils.mdx index 7f1e021d2997e..9fbc74f2281fa 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: 2023-04-21 +date: 2023-04-24 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 7b8ea7f8fa153..d99c895cfbb07 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: 2023-04-21 +date: 2023-04-24 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 bbaae18e81926..fc1a7126299da 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: 2023-04-21 +date: 2023-04-24 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 b22479ca8cb62..5b42ba79461cd 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: 2023-04-21 +date: 2023-04-24 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 e959b8f8857f1..c538168844e59 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: 2023-04-21 +date: 2023-04-24 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 4dce3d0bcc354..7c066060f57b3 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: 2023-04-21 +date: 2023-04-24 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 b3241f80861cf..57b4b0757c666 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-svg'] --- import kbnSharedSvgObj from './kbn_shared_svg.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_solution.mdx b/api_docs/kbn_shared_ux_avatar_solution.mdx index 7fdc62d57e0e5..0e0a7de059854 100644 --- a/api_docs/kbn_shared_ux_avatar_solution.mdx +++ b/api_docs/kbn_shared_ux_avatar_solution.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-solution title: "@kbn/shared-ux-avatar-solution" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-solution plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-solution'] --- import kbnSharedUxAvatarSolutionObj from './kbn_shared_ux_avatar_solution.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx index 74b168d876714..cbe1bbc592105 100644 --- a/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx +++ b/api_docs/kbn_shared_ux_avatar_user_profile_components.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-avatar-user-profile-components title: "@kbn/shared-ux-avatar-user-profile-components" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-avatar-user-profile-components plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-avatar-user-profile-components'] --- import kbnSharedUxAvatarUserProfileComponentsObj from './kbn_shared_ux_avatar_user_profile_components.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx index c67019c024447..fd754396fe38d 100644 --- a/api_docs/kbn_shared_ux_button_exit_full_screen.mdx +++ b/api_docs/kbn_shared_ux_button_exit_full_screen.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-button-exit-full-screen title: "@kbn/shared-ux-button-exit-full-screen" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-button-exit-full-screen plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-button-exit-full-screen'] --- import kbnSharedUxButtonExitFullScreenObj from './kbn_shared_ux_button_exit_full_screen.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 523f937ac0308..704bf0d55a588 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: 2023-04-21 +date: 2023-04-24 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 7e026fc462e17..d5cbbf1440008 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: 2023-04-21 +date: 2023-04-24 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 6744cc7da9aa8..6e187d6a67d6d 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: 2023-04-21 +date: 2023-04-24 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 604622664685d..f1eafa0073b75 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: 2023-04-21 +date: 2023-04-24 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_file_context.mdx b/api_docs/kbn_shared_ux_file_context.mdx index 2218b967c0672..345346e831748 100644 --- a/api_docs/kbn_shared_ux_file_context.mdx +++ b/api_docs/kbn_shared_ux_file_context.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-context title: "@kbn/shared-ux-file-context" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-context plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-context'] --- import kbnSharedUxFileContextObj from './kbn_shared_ux_file_context.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image.mdx b/api_docs/kbn_shared_ux_file_image.mdx index 961468e71949a..e242bd651fa0d 100644 --- a/api_docs/kbn_shared_ux_file_image.mdx +++ b/api_docs/kbn_shared_ux_file_image.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image title: "@kbn/shared-ux-file-image" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image'] --- import kbnSharedUxFileImageObj from './kbn_shared_ux_file_image.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_image_mocks.mdx b/api_docs/kbn_shared_ux_file_image_mocks.mdx index f82ac77b98773..4e391b9d8ab27 100644 --- a/api_docs/kbn_shared_ux_file_image_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_image_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-image-mocks title: "@kbn/shared-ux-file-image-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-image-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-image-mocks'] --- import kbnSharedUxFileImageMocksObj from './kbn_shared_ux_file_image_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_mocks.mdx b/api_docs/kbn_shared_ux_file_mocks.mdx index 1ab3bafb4ee5f..662c634769dd1 100644 --- a/api_docs/kbn_shared_ux_file_mocks.mdx +++ b/api_docs/kbn_shared_ux_file_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-mocks title: "@kbn/shared-ux-file-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-mocks'] --- import kbnSharedUxFileMocksObj from './kbn_shared_ux_file_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_picker.mdx b/api_docs/kbn_shared_ux_file_picker.mdx index f48a321a34b54..1e22cbdd34b00 100644 --- a/api_docs/kbn_shared_ux_file_picker.mdx +++ b/api_docs/kbn_shared_ux_file_picker.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-picker title: "@kbn/shared-ux-file-picker" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-picker plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-picker'] --- import kbnSharedUxFilePickerObj from './kbn_shared_ux_file_picker.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_types.mdx b/api_docs/kbn_shared_ux_file_types.mdx index cf77561da9e6e..43c10d7423ef7 100644 --- a/api_docs/kbn_shared_ux_file_types.mdx +++ b/api_docs/kbn_shared_ux_file_types.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-types title: "@kbn/shared-ux-file-types" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-types plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-types'] --- import kbnSharedUxFileTypesObj from './kbn_shared_ux_file_types.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_upload.mdx b/api_docs/kbn_shared_ux_file_upload.mdx index 84e8d32366c10..7a684a3ddf9b6 100644 --- a/api_docs/kbn_shared_ux_file_upload.mdx +++ b/api_docs/kbn_shared_ux_file_upload.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-upload title: "@kbn/shared-ux-file-upload" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-upload plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-upload'] --- import kbnSharedUxFileUploadObj from './kbn_shared_ux_file_upload.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_file_util.mdx b/api_docs/kbn_shared_ux_file_util.mdx index d79e2ec066afd..7a12cb245836a 100644 --- a/api_docs/kbn_shared_ux_file_util.mdx +++ b/api_docs/kbn_shared_ux_file_util.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-file-util title: "@kbn/shared-ux-file-util" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-file-util plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-file-util'] --- import kbnSharedUxFileUtilObj from './kbn_shared_ux_file_util.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_link_redirect_app.mdx b/api_docs/kbn_shared_ux_link_redirect_app.mdx index 35763b229e400..dc82b967602d2 100644 --- a/api_docs/kbn_shared_ux_link_redirect_app.mdx +++ b/api_docs/kbn_shared_ux_link_redirect_app.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-link-redirect-app title: "@kbn/shared-ux-link-redirect-app" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-link-redirect-app plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-link-redirect-app'] --- import kbnSharedUxLinkRedirectAppObj from './kbn_shared_ux_link_redirect_app.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 714f376ffb7c5..18b157f050418 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: 2023-04-21 +date: 2023-04-24 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_markdown.mdx b/api_docs/kbn_shared_ux_markdown.mdx index f4f56efd30485..8d43b2bc2609d 100644 --- a/api_docs/kbn_shared_ux_markdown.mdx +++ b/api_docs/kbn_shared_ux_markdown.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown title: "@kbn/shared-ux-markdown" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown'] --- import kbnSharedUxMarkdownObj from './kbn_shared_ux_markdown.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_markdown_mocks.mdx b/api_docs/kbn_shared_ux_markdown_mocks.mdx index 5d46068526cc8..6ad8500d4ffa1 100644 --- a/api_docs/kbn_shared_ux_markdown_mocks.mdx +++ b/api_docs/kbn_shared_ux_markdown_mocks.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-markdown-mocks title: "@kbn/shared-ux-markdown-mocks" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-markdown-mocks plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-markdown-mocks'] --- import kbnSharedUxMarkdownMocksObj from './kbn_shared_ux_markdown_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 a703893f75dec..da16a3294719e 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: 2023-04-21 +date: 2023-04-24 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 c6146d86359c0..a79ab025d8f39 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: 2023-04-21 +date: 2023-04-24 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 2fc62d3203dab..675d8ca547baa 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: 2023-04-21 +date: 2023-04-24 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 e4a702e9d4de6..87063d4968ca9 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: 2023-04-21 +date: 2023-04-24 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 0281befb594ed..d83b54deb377f 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: 2023-04-21 +date: 2023-04-24 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 bc4addd3208ad..a915fe7ce8d2f 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: 2023-04-21 +date: 2023-04-24 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 0144374aab4fb..d386725c0f96e 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: 2023-04-21 +date: 2023-04-24 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 da46bad1d78e5..b609f61949e3a 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: 2023-04-21 +date: 2023-04-24 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 4681ccfbf18d8..3b8a6aea768c4 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: 2023-04-21 +date: 2023-04-24 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 ac7fbb18cee1d..b549d3be8ce48 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: 2023-04-21 +date: 2023-04-24 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 c5f2135a7e18e..57b7f77917dbf 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: 2023-04-21 +date: 2023-04-24 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 90f8c67503a8a..cde75adfcbd03 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: 2023-04-21 +date: 2023-04-24 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 0c4bc9ae73323..449ec14d0e89b 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: 2023-04-21 +date: 2023-04-24 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_prompt_not_found.mdx b/api_docs/kbn_shared_ux_prompt_not_found.mdx index da5e9ae1eb9a1..54e8a60fb3a71 100644 --- a/api_docs/kbn_shared_ux_prompt_not_found.mdx +++ b/api_docs/kbn_shared_ux_prompt_not_found.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-shared-ux-prompt-not-found title: "@kbn/shared-ux-prompt-not-found" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/shared-ux-prompt-not-found plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-prompt-not-found'] --- import kbnSharedUxPromptNotFoundObj from './kbn_shared_ux_prompt_not_found.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router.mdx b/api_docs/kbn_shared_ux_router.mdx index aa816570472a8..64431ebf0a9f6 100644 --- a/api_docs/kbn_shared_ux_router.mdx +++ b/api_docs/kbn_shared_ux_router.mdx @@ -8,7 +8,7 @@ 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router'] --- import kbnSharedUxRouterObj from './kbn_shared_ux_router.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_router_mocks.mdx b/api_docs/kbn_shared_ux_router_mocks.mdx index 135cdbf742f31..8f903a6a4599b 100644 --- a/api_docs/kbn_shared_ux_router_mocks.mdx +++ b/api_docs/kbn_shared_ux_router_mocks.mdx @@ -8,7 +8,7 @@ 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-router-mocks'] --- import kbnSharedUxRouterMocksObj from './kbn_shared_ux_router_mocks.devdocs.json'; diff --git a/api_docs/kbn_shared_ux_storybook_config.mdx b/api_docs/kbn_shared_ux_storybook_config.mdx index 9ca85a30f64b6..aa8aeb5ed628b 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: 2023-04-21 +date: 2023-04-24 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 ce99698fd9f1b..1f594a6b27503 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: 2023-04-21 +date: 2023-04-24 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 0e8f337108e82..35030e8f65b69 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/shared-ux-utility'] --- import kbnSharedUxUtilityObj from './kbn_shared_ux_utility.devdocs.json'; diff --git a/api_docs/kbn_slo_schema.mdx b/api_docs/kbn_slo_schema.mdx index 97a606deb5c99..b86313f3a585f 100644 --- a/api_docs/kbn_slo_schema.mdx +++ b/api_docs/kbn_slo_schema.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-slo-schema title: "@kbn/slo-schema" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/slo-schema plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/slo-schema'] --- import kbnSloSchemaObj from './kbn_slo_schema.devdocs.json'; diff --git a/api_docs/kbn_some_dev_log.mdx b/api_docs/kbn_some_dev_log.mdx index d1a8eebc6a352..8a4c14deb442e 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/some-dev-log'] --- import kbnSomeDevLogObj from './kbn_some_dev_log.devdocs.json'; diff --git a/api_docs/kbn_std.mdx b/api_docs/kbn_std.mdx index 0217dc9a965db..8a33d6033f83b 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: 2023-04-21 +date: 2023-04-24 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 b4b67a76d6345..8bb33b6f9fc6c 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: 2023-04-21 +date: 2023-04-24 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 fd6b8221cc8fd..0dcbe8b65314a 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: 2023-04-21 +date: 2023-04-24 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 a2a8c3a870b1a..009ae2d00a260 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/telemetry-tools'] --- import kbnTelemetryToolsObj from './kbn_telemetry_tools.devdocs.json'; diff --git a/api_docs/kbn_test.mdx b/api_docs/kbn_test.mdx index 245f8573362f4..077c288dbbd06 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test'] --- import kbnTestObj from './kbn_test.devdocs.json'; diff --git a/api_docs/kbn_test_jest_helpers.mdx b/api_docs/kbn_test_jest_helpers.mdx index 7c8dcb3038c28..ac6e29488cd2d 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-jest-helpers'] --- import kbnTestJestHelpersObj from './kbn_test_jest_helpers.devdocs.json'; diff --git a/api_docs/kbn_test_subj_selector.mdx b/api_docs/kbn_test_subj_selector.mdx index f4a6b85e91f0c..4b38e19d2c144 100644 --- a/api_docs/kbn_test_subj_selector.mdx +++ b/api_docs/kbn_test_subj_selector.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-test-subj-selector title: "@kbn/test-subj-selector" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/test-subj-selector plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/test-subj-selector'] --- import kbnTestSubjSelectorObj from './kbn_test_subj_selector.devdocs.json'; diff --git a/api_docs/kbn_tooling_log.mdx b/api_docs/kbn_tooling_log.mdx index 2da057aa80b43..1e093e1c87a67 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/tooling-log'] --- import kbnToolingLogObj from './kbn_tooling_log.devdocs.json'; diff --git a/api_docs/kbn_ts_projects.mdx b/api_docs/kbn_ts_projects.mdx index efa271163a527..ec139fac154dc 100644 --- a/api_docs/kbn_ts_projects.mdx +++ b/api_docs/kbn_ts_projects.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ts-projects title: "@kbn/ts-projects" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ts-projects plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ts-projects'] --- import kbnTsProjectsObj from './kbn_ts_projects.devdocs.json'; diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index e36f3796a6a55..c335bc8053866 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: 2023-04-21 +date: 2023-04-24 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_actions_browser.mdx b/api_docs/kbn_ui_actions_browser.mdx index 3cd1a6aef2f77..e4f4818385008 100644 --- a/api_docs/kbn_ui_actions_browser.mdx +++ b/api_docs/kbn_ui_actions_browser.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-actions-browser title: "@kbn/ui-actions-browser" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-actions-browser plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-actions-browser'] --- import kbnUiActionsBrowserObj from './kbn_ui_actions_browser.devdocs.json'; diff --git a/api_docs/kbn_ui_shared_deps_src.mdx b/api_docs/kbn_ui_shared_deps_src.mdx index 11107207bbef6..cfc3a2c905dbb 100644 --- a/api_docs/kbn_ui_shared_deps_src.mdx +++ b/api_docs/kbn_ui_shared_deps_src.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/kbn-ui-shared-deps-src title: "@kbn/ui-shared-deps-src" image: https://source.unsplash.com/400x175/?github description: API docs for the @kbn/ui-shared-deps-src plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-shared-deps-src'] --- import kbnUiSharedDepsSrcObj from './kbn_ui_shared_deps_src.devdocs.json'; diff --git a/api_docs/kbn_ui_theme.mdx b/api_docs/kbn_ui_theme.mdx index a6c35e39c2b54..2306fe2c1523e 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/ui-theme'] --- import kbnUiThemeObj from './kbn_ui_theme.devdocs.json'; diff --git a/api_docs/kbn_url_state.devdocs.json b/api_docs/kbn_url_state.devdocs.json new file mode 100644 index 0000000000000..e7659b142aa8e --- /dev/null +++ b/api_docs/kbn_url_state.devdocs.json @@ -0,0 +1,99 @@ +{ + "id": "@kbn/url-state", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl", + "type": "Function", + "tags": [], + "label": "useSyncToUrl", + "description": [ + "\nSync any object with browser query string using @knb/rison" + ], + "signature": [ + "(key: string, restore: (data: TValueToSerialize) => void, cleanupOnHistoryNavigation?: boolean) => (valueToSerialize?: TValueToSerialize | undefined) => void" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "deprecated": false, + "trackAdoption": false, + "children": [ + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl.$1", + "type": "string", + "tags": [], + "label": "key", + "description": [ + "query string param to use" + ], + "signature": [ + "string" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl.$2", + "type": "Function", + "tags": [], + "label": "restore", + "description": [ + "use this to handle restored state" + ], + "signature": [ + "(data: TValueToSerialize) => void" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "deprecated": false, + "trackAdoption": false, + "isRequired": true + }, + { + "parentPluginId": "@kbn/url-state", + "id": "def-common.useSyncToUrl.$3", + "type": "boolean", + "tags": [], + "label": "cleanupOnHistoryNavigation", + "description": [ + "use history events to cleanup state on back / forward naviation. true by default" + ], + "signature": [ + "boolean" + ], + "path": "packages/kbn-url-state/use_sync_to_url.ts", + "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_url_state.mdx b/api_docs/kbn_url_state.mdx new file mode 100644 index 0000000000000..1ecdc4cf5f6b8 --- /dev/null +++ b/api_docs/kbn_url_state.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: kibKbnUrlStatePluginApi +slug: /kibana-dev-docs/api/kbn-url-state +title: "@kbn/url-state" +image: https://source.unsplash.com/400x175/?github +description: API docs for the @kbn/url-state plugin +date: 2023-04-24 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@kbn/url-state'] +--- +import kbnUrlStateObj from './kbn_url_state.devdocs.json'; + + + +Contact [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 0 | 0 | + +## Common + +### Functions + + diff --git a/api_docs/kbn_user_profile_components.mdx b/api_docs/kbn_user_profile_components.mdx index 99b0de88ef5ef..9678a8f3d941e 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: 2023-04-21 +date: 2023-04-24 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 d7c2e09558b57..0847fe990eba3 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: 2023-04-21 +date: 2023-04-24 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 18348076c4a98..bd7b6dc7e7ca0 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: 2023-04-21 +date: 2023-04-24 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 a743985adbe80..3bb37ce9fb475 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: 2023-04-21 +date: 2023-04-24 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 73c55462f3c81..89bec78fbe1d0 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: 2023-04-21 +date: 2023-04-24 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 e20a39ed1d8b5..35b6ab39c77a0 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: 2023-04-21 +date: 2023-04-24 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 cb370c5b41c00..277ae838dc455 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: 2023-04-21 +date: 2023-04-24 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 e4252f4bb1d23..9710a9b016f88 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: 2023-04-21 +date: 2023-04-24 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 f13e3882eba31..e9b23fb4642c8 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: 2023-04-21 +date: 2023-04-24 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 0a8741d076772..7e35673a3ef08 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: 2023-04-21 +date: 2023-04-24 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 b9b65d3c94bc6..18e54115992df 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: 2023-04-21 +date: 2023-04-24 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 e3bb7df026945..f791025b3e353 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: 2023-04-21 +date: 2023-04-24 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 704afaaf19d4d..348e30ab776d8 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'licensing'] --- import licensingObj from './licensing.devdocs.json'; diff --git a/api_docs/lists.devdocs.json b/api_docs/lists.devdocs.json index cf0f553770aee..a5d1cf263468f 100644 --- a/api_docs/lists.devdocs.json +++ b/api_docs/lists.devdocs.json @@ -736,7 +736,7 @@ "\nCreate the Trusted Apps Agnostic list if it does not yet exist (`null` is returned if it does exist)" ], "signature": [ - "({ listId, namespaceType, }: ", + "({ list, namespaceType, includeExpiredExceptions, }: ", "DuplicateExceptionListOptions", ") => Promise<{ _version: string | undefined; created_at: string; created_by: string; description: string; id: string; immutable: boolean; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"endpoint\" | \"detection\" | \"rule_default\" | \"endpoint_trusted_apps\" | \"endpoint_events\" | \"endpoint_host_isolation_exceptions\" | \"endpoint_blocklists\"; updated_at: string; updated_by: string; version: number; } | null>" ], @@ -749,7 +749,7 @@ "id": "def-server.ExceptionListClient.duplicateExceptionListAndItems.$1", "type": "Object", "tags": [], - "label": "{\n listId,\n namespaceType,\n }", + "label": "{\n list,\n namespaceType,\n includeExpiredExceptions,\n }", "description": [], "signature": [ "DuplicateExceptionListOptions" diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 6a3f788c87be3..7a4f5df515fb3 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: 2023-04-21 +date: 2023-04-24 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 f2a3ee2fecf34..e86efc700e7ea 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: 2023-04-21 +date: 2023-04-24 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 3a3a5d8d917d1..d5ef557d05116 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: 2023-04-21 +date: 2023-04-24 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 4205c9b472f0b..97034dddf8ac6 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'mapsEms'] --- import mapsEmsObj from './maps_ems.devdocs.json'; diff --git a/api_docs/ml.devdocs.json b/api_docs/ml.devdocs.json index a235055918f57..a714fc22ec0c9 100644 --- a/api_docs/ml.devdocs.json +++ b/api_docs/ml.devdocs.json @@ -3306,41 +3306,6 @@ "returnComment": [], "initialIsOpen": false }, - { - "parentPluginId": "ml", - "id": "def-common.extractErrorMessage", - "type": "Function", - "tags": [], - "label": "extractErrorMessage", - "description": [], - "signature": [ - "(error: ", - "ErrorType", - ") => string" - ], - "path": "x-pack/plugins/ml/common/util/errors/process_errors.ts", - "deprecated": false, - "trackAdoption": false, - "children": [ - { - "parentPluginId": "ml", - "id": "def-common.extractErrorMessage.$1", - "type": "CompoundType", - "tags": [], - "label": "error", - "description": [], - "signature": [ - "ErrorType" - ], - "path": "x-pack/plugins/ml/common/util/errors/process_errors.ts", - "deprecated": false, - "trackAdoption": false, - "isRequired": false - } - ], - "returnComment": [], - "initialIsOpen": false - }, { "parentPluginId": "ml", "id": "def-common.getDefaultCapabilities", diff --git a/api_docs/ml.mdx b/api_docs/ml.mdx index d7fab7ce5ee29..d977d47492384 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'ml'] --- import mlObj from './ml.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) for questi | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 259 | 9 | 83 | 40 | +| 257 | 9 | 81 | 39 | ## Client diff --git a/api_docs/monitoring.mdx b/api_docs/monitoring.mdx index fbb5d4166d7c0..90c474ac78f1a 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: 2023-04-21 +date: 2023-04-24 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 390050d8aca21..8d7ae64cb77ba 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: 2023-04-21 +date: 2023-04-24 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 c1da45703e1e0..59a2eae887fb5 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: 2023-04-21 +date: 2023-04-24 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 ea9412b022b55..f1ec49c987768 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'newsfeed'] --- import newsfeedObj from './newsfeed.devdocs.json'; diff --git a/api_docs/notifications.mdx b/api_docs/notifications.mdx index 45300a85184d2..bf6aade55bc14 100644 --- a/api_docs/notifications.mdx +++ b/api_docs/notifications.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/notifications title: "notifications" image: https://source.unsplash.com/400x175/?github description: API docs for the notifications plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'notifications'] --- import notificationsObj from './notifications.devdocs.json'; diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index caed04a1ba9d5..01cac0e1d423a 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observability'] --- import observabilityObj from './observability.devdocs.json'; diff --git a/api_docs/observability_shared.mdx b/api_docs/observability_shared.mdx index 85db14ed31dc3..321906e5ba6ff 100644 --- a/api_docs/observability_shared.mdx +++ b/api_docs/observability_shared.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/observabilityShared title: "observabilityShared" image: https://source.unsplash.com/400x175/?github description: API docs for the observabilityShared plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'observabilityShared'] --- import observabilitySharedObj from './observability_shared.devdocs.json'; diff --git a/api_docs/osquery.mdx b/api_docs/osquery.mdx index 57b293ee7b2e3..171effe7c5467 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'osquery'] --- import osqueryObj from './osquery.devdocs.json'; diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index dc3d7eab17a06..e0ee93073348b 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: 2023-04-21 +date: 2023-04-24 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 | |--------------|----------|------------------------| -| 595 | 491 | 37 | +| 597 | 493 | 37 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 69056 | 526 | 59639 | 1335 | +| 69151 | 525 | 59683 | 1333 | ## Plugin Directory @@ -30,7 +30,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 259 | 8 | 254 | 26 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 36 | 1 | 32 | 2 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | AIOps plugin maintained by ML team. | 39 | 0 | 24 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 603 | 1 | 582 | 42 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 606 | 1 | 585 | 42 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 43 | 0 | 43 | 110 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | Asset manager plugin for entity assets (inventory, topology, etc) | 3 | 0 | 3 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 9 | 0 | 9 | 0 | @@ -48,7 +48,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | cloudLinks | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | Adds the links to the Elastic Cloud console | 0 | 0 | 0 | 0 | | | [@elastic/kibana-cloud-security-posture](https://github.com/orgs/elastic/teams/kibana-cloud-security-posture) | The cloud security posture plugin | 17 | 0 | 2 | 2 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 13 | 0 | 13 | 1 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Content management app | 143 | 0 | 124 | 7 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Content management app | 149 | 0 | 126 | 6 | | | [@elastic/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 | 301 | 0 | 294 | 13 | | crossClusterReplication | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | | customBranding | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Enables customization of Kibana | 0 | 0 | 0 | 0 | @@ -99,7 +99,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | globalSearchProviders | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 0 | 0 | 0 | 0 | | graph | [@elastic/kibana-visualizations](https://github.com/orgs/elastic/teams/kibana-visualizations) | - | 0 | 0 | 0 | 0 | | grokdebugger | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 0 | 0 | 0 | 0 | -| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Guided onboarding framework | 56 | 0 | 55 | 0 | +| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | Guided onboarding framework | 57 | 0 | 56 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 143 | 0 | 104 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Image embeddable | 3 | 0 | 3 | 1 | | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 4 | 0 | 4 | 0 | @@ -123,7 +123,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/platform-deployment-management](https://github.com/orgs/elastic/teams/platform-deployment-management) | - | 41 | 0 | 41 | 6 | | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 269 | 0 | 268 | 29 | | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 67 | 0 | 67 | 0 | -| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the machine learning features provided by Elastic. | 259 | 9 | 83 | 40 | +| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the machine learning features provided by Elastic. | 257 | 9 | 81 | 39 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | - | 15 | 3 | 13 | 1 | | | [@elastic/infra-monitoring-ui](https://github.com/orgs/elastic/teams/infra-monitoring-ui) | - | 9 | 0 | 9 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 34 | 0 | 34 | 2 | @@ -167,7 +167,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 257 | 1 | 214 | 20 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [@elastic/kibana-localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | -| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 540 | 10 | 511 | 49 | +| | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 542 | 10 | 513 | 49 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Adds UI Actions service to Kibana | 134 | 2 | 92 | 9 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | Extends UI Actions plugin with more functionality | 206 | 0 | 140 | 9 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | Contains functionality for the field list which can be integrated into apps | 296 | 0 | 270 | 6 | @@ -214,7 +214,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 18 | 0 | 2 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 27 | 0 | 27 | 3 | -| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 154 | 0 | 154 | 17 | +| | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 153 | 0 | 153 | 17 | | | [@elastic/apm-ui](https://github.com/orgs/elastic/teams/apm-ui) | - | 11 | 0 | 11 | 0 | | | [@elastic/kibana-qa](https://github.com/orgs/elastic/teams/kibana-qa) | - | 12 | 0 | 12 | 0 | | | [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-ops) | - | 19 | 0 | 17 | 0 | @@ -408,14 +408,14 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 255 | 1 | 197 | 15 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 12 | 0 | 12 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 1 | 0 | -| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 13 | 0 | 4 | 3 | +| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 33 | 0 | 13 | 3 | | | [@elastic/kibana-data-discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | - | 20 | 0 | 16 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 0 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 29 | 0 | 29 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 1 | 0 | 0 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 10 | 0 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 10 | 0 | 10 | 0 | -| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | - | 52 | 0 | 50 | 3 | +| | [@elastic/platform-onboarding](https://github.com/orgs/elastic/teams/platform-onboarding) | - | 54 | 0 | 52 | 3 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 33 | 3 | 24 | 6 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 3 | 0 | 3 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 1 | 0 | 1 | 0 | @@ -437,6 +437,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/kibana-gis](https://github.com/orgs/elastic/teams/kibana-gis) | - | 534 | 1 | 1 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 93 | 2 | 61 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 52 | 0 | 4 | 0 | +| | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 36 | 0 | 8 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 2 | 0 | 0 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 3 | 0 | 2 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 5 | 0 | 3 | 0 | @@ -449,7 +450,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 8 | 1 | 8 | 0 | | | [@elastic/ml-ui](https://github.com/orgs/elastic/teams/ml-ui) | - | 31 | 1 | 24 | 1 | | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 71 | 0 | 69 | 3 | -| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 53 | 1 | 48 | 0 | +| | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 55 | 1 | 50 | 0 | | | [@elastic/actionable-observability](https://github.com/orgs/elastic/teams/actionable-observability) | - | 7 | 0 | 7 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 45 | 0 | 45 | 10 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 51 | 5 | 34 | 0 | @@ -475,13 +476,13 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 104 | 0 | 93 | 1 | | | [@elastic/security-threat-hunting-explore](https://github.com/orgs/elastic/teams/security-threat-hunting-explore) | - | 20 | 0 | 15 | 4 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 15 | 0 | 7 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 141 | 0 | 122 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 516 | 1 | 503 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 147 | 0 | 125 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 528 | 0 | 515 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 65 | 0 | 36 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 28 | 0 | 21 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 67 | 0 | 64 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 69 | 0 | 65 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 35 | 0 | 23 | 0 | -| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 60 | 0 | 47 | 0 | +| | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 62 | 0 | 49 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 210 | 10 | 163 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 26 | 0 | 23 | 0 | | | [@elastic/security-solution-platform](https://github.com/orgs/elastic/teams/security-solution-platform) | - | 120 | 0 | 116 | 0 | @@ -542,6 +543,7 @@ tags: ['contributor', 'dev', 'apidocs', 'kibana'] | | [@elastic/appex-sharedux](https://github.com/orgs/elastic/teams/appex-sharedux) | - | 18 | 0 | 8 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 45 | 0 | 36 | 0 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 7 | 0 | 6 | 0 | +| | [@elastic/security-threat-hunting-investigations](https://github.com/orgs/elastic/teams/security-threat-hunting-investigations) | - | 4 | 0 | 0 | 0 | | | [@elastic/kibana-security](https://github.com/orgs/elastic/teams/kibana-security) | - | 58 | 0 | 5 | 0 | | | [@elastic/kibana-core](https://github.com/orgs/elastic/teams/kibana-core) | - | 36 | 0 | 15 | 1 | | | [@elastic/kibana-operations](https://github.com/orgs/elastic/teams/kibana-operations) | - | 2 | 0 | 2 | 0 | diff --git a/api_docs/presentation_util.mdx b/api_docs/presentation_util.mdx index bd5fa373355d3..bb5dd22da4a5d 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'presentationUtil'] --- import presentationUtilObj from './presentation_util.devdocs.json'; diff --git a/api_docs/profiling.mdx b/api_docs/profiling.mdx index 94ec36d0e142d..fa7c514839f1d 100644 --- a/api_docs/profiling.mdx +++ b/api_docs/profiling.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/profiling title: "profiling" image: https://source.unsplash.com/400x175/?github description: API docs for the profiling plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'profiling'] --- import profilingObj from './profiling.devdocs.json'; diff --git a/api_docs/remote_clusters.mdx b/api_docs/remote_clusters.mdx index 4d93f65d4f588..052f465958e43 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: 2023-04-21 +date: 2023-04-24 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 dc655be6ea3b1..b3e643f23c824 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: 2023-04-21 +date: 2023-04-24 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 22004d4715863..3ad92d06484ef 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: 2023-04-21 +date: 2023-04-24 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 7af25a7132259..2230c7e475fbf 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: 2023-04-21 +date: 2023-04-24 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 bf014f1590fc2..fbb641e9ec07b 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: 2023-04-21 +date: 2023-04-24 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 1ce68779192c7..4775f7bab3e25 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: 2023-04-21 +date: 2023-04-24 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 ba213be4dd0b6..c5cc15bc31edd 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: 2023-04-21 +date: 2023-04-24 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 b3d06632c1757..8ed4d9ce7ea8b 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: 2023-04-21 +date: 2023-04-24 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 28ee6a6fdb1e9..28d4dfa912c94 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: 2023-04-21 +date: 2023-04-24 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 50a27cc71b42c..c4e422b6d4bf5 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: 2023-04-21 +date: 2023-04-24 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 2f0a64f88e254..b41919c5f029f 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: 2023-04-21 +date: 2023-04-24 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 b87efc25bb18e..eee83610aebe7 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: 2023-04-21 +date: 2023-04-24 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 179ea1d1861aa..8b105e90157ca 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: 2023-04-21 +date: 2023-04-24 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 b93687377e8a6..6a3ef50e5d725 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: 2023-04-21 +date: 2023-04-24 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 d193ee3ae36cf..49cd868cf6aee 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: 2023-04-21 +date: 2023-04-24 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 863bb87c48c26..89f21abe997f4 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: 2023-04-21 +date: 2023-04-24 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 2bb9768f311c2..41725af018c9e 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: 2023-04-21 +date: 2023-04-24 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 44b56f58ba474..582bfce7f3253 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: 2023-04-21 +date: 2023-04-24 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 563274dd2f750..2467dff1cd464 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: 2023-04-21 +date: 2023-04-24 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 bfd22efb5d897..f2fd4e7fff1d3 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackAlerts'] --- import stackAlertsObj from './stack_alerts.devdocs.json'; diff --git a/api_docs/stack_connectors.mdx b/api_docs/stack_connectors.mdx index 7426f4bf3c2e2..12ffc30623e60 100644 --- a/api_docs/stack_connectors.mdx +++ b/api_docs/stack_connectors.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/stackConnectors title: "stackConnectors" image: https://source.unsplash.com/400x175/?github description: API docs for the stackConnectors plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'stackConnectors'] --- import stackConnectorsObj from './stack_connectors.devdocs.json'; diff --git a/api_docs/task_manager.mdx b/api_docs/task_manager.mdx index 07663807996de..ec588948cbe18 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: 2023-04-21 +date: 2023-04-24 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 507775a14db43..439a2431b4376 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: 2023-04-21 +date: 2023-04-24 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 b0d73ccf96937..a445edd5a138f 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: 2023-04-21 +date: 2023-04-24 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 3870cdba63b35..29ee2d6178438 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: 2023-04-21 +date: 2023-04-24 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 27c23a0eb88db..fbe7face22441 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: 2023-04-21 +date: 2023-04-24 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 b54750be2d606..5ca7b1d27e7d4 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: 2023-04-21 +date: 2023-04-24 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 acb04c0a79a88..41df96ec361dd 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: 2023-04-21 +date: 2023-04-24 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 a5cb1bf5bb0f1..e0a7c8eca1a70 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] --- import transformObj from './transform.devdocs.json'; diff --git a/api_docs/triggers_actions_ui.devdocs.json b/api_docs/triggers_actions_ui.devdocs.json index 8b881a6bf3005..ae29d4e3e787a 100644 --- a/api_docs/triggers_actions_ui.devdocs.json +++ b/api_docs/triggers_actions_ui.devdocs.json @@ -2831,6 +2831,20 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertStatus.maintenanceWindowIds", + "type": "Array", + "tags": [], + "label": "maintenanceWindowIds", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false @@ -3045,6 +3059,17 @@ "path": "x-pack/plugins/alerting/common/alert_summary.ts", "deprecated": false, "trackAdoption": false + }, + { + "parentPluginId": "triggersActionsUi", + "id": "def-public.AlertSummary.revision", + "type": "number", + "tags": [], + "label": "revision", + "description": [], + "path": "x-pack/plugins/alerting/common/alert_summary.ts", + "deprecated": false, + "trackAdoption": false } ], "initialIsOpen": false diff --git a/api_docs/triggers_actions_ui.mdx b/api_docs/triggers_actions_ui.mdx index fc92b4f9ff615..0dfa7fa50555b 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'triggersActionsUi'] --- import triggersActionsUiObj from './triggers_actions_ui.devdocs.json'; @@ -21,7 +21,7 @@ Contact [@elastic/response-ops](https://github.com/orgs/elastic/teams/response-o | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 540 | 10 | 511 | 49 | +| 542 | 10 | 513 | 49 | ## Client diff --git a/api_docs/ui_actions.mdx b/api_docs/ui_actions.mdx index 2fe2650841250..ba54dfe3328c8 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: 2023-04-21 +date: 2023-04-24 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 a46c6ed8f86f6..0111328be34a0 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: 2023-04-21 +date: 2023-04-24 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 604ae78db8a3e..97fceac316e4d 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedFieldList'] --- import unifiedFieldListObj from './unified_field_list.devdocs.json'; diff --git a/api_docs/unified_histogram.mdx b/api_docs/unified_histogram.mdx index 33f08e0514e8c..e0acc17411f17 100644 --- a/api_docs/unified_histogram.mdx +++ b/api_docs/unified_histogram.mdx @@ -8,7 +8,7 @@ slug: /kibana-dev-docs/api/unifiedHistogram title: "unifiedHistogram" image: https://source.unsplash.com/400x175/?github description: API docs for the unifiedHistogram plugin -date: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'unifiedHistogram'] --- import unifiedHistogramObj from './unified_histogram.devdocs.json'; diff --git a/api_docs/unified_search.devdocs.json b/api_docs/unified_search.devdocs.json index 05f2215a9a913..8f697ba358c0c 100644 --- a/api_docs/unified_search.devdocs.json +++ b/api_docs/unified_search.devdocs.json @@ -487,7 +487,7 @@ "OnSaveTextLanguageQueryProps", ") => void) | undefined; showSubmitButton?: boolean | undefined; submitButtonStyle?: \"full\" | \"auto\" | \"iconOnly\" | undefined; suggestionsSize?: ", "SuggestionsListSize", - " | undefined; isScreenshotMode?: boolean | undefined; onFiltersUpdated?: ((filters: ", + " | undefined; isScreenshotMode?: boolean | undefined; submitOnBlur?: boolean | undefined; onFiltersUpdated?: ((filters: ", { "pluginId": "@kbn/es-query", "scope": "common", diff --git a/api_docs/unified_search.mdx b/api_docs/unified_search.mdx index 5c23136a98a7a..9655a20a1dda9 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: 2023-04-21 +date: 2023-04-24 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 1b5beadfecf13..f70714d1e5938 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: 2023-04-21 +date: 2023-04-24 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 b6d4f177afce2..fb8870f2325d2 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: 2023-04-21 +date: 2023-04-24 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 919ee29502898..569cd4f8baf09 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: 2023-04-21 +date: 2023-04-24 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 de9da5f50c4dd..71a2488190eb8 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: 2023-04-21 +date: 2023-04-24 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 d17bcc483e1c3..aa3747f8476f0 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: 2023-04-21 +date: 2023-04-24 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 d73b04c29be18..24c99b2aa19f8 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: 2023-04-21 +date: 2023-04-24 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 dfab14fdd94de..d7649559452e8 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: 2023-04-21 +date: 2023-04-24 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 096f49cdb1541..c493a75a57076 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: 2023-04-21 +date: 2023-04-24 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 e36aeeed77738..b954ce0b719d2 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: 2023-04-21 +date: 2023-04-24 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 eba3c262b3db4..b98b43d726106 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: 2023-04-21 +date: 2023-04-24 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 504196c391a34..7a80a887ba186 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: 2023-04-21 +date: 2023-04-24 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 bc9c59bd6a49c..b760568fac4e3 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: 2023-04-21 +date: 2023-04-24 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 b8ac18365cd3c..27174661c8845 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: 2023-04-21 +date: 2023-04-24 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 409866a3315d4..f64e0c6adbde7 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: 2023-04-21 +date: 2023-04-24 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 348c6c015d348..a8c2abb735484 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: 2023-04-21 +date: 2023-04-24 tags: ['contributor', 'dev', 'apidocs', 'kibana', 'visualizations'] --- import visualizationsObj from './visualizations.devdocs.json'; diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 65bc85a7d7133..7d6cc75282a2f 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -30,6 +30,7 @@ or as needed when you're creating a rule. For example: [role="screenshot"] image::management/connectors/images/email-connector.png[Email connector] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. [float] [[email-connector-configuration]] @@ -182,6 +183,7 @@ as you're creating or editing the connector in {kib}. For example: [role="screenshot"] image::management/connectors/images/email-params-test.png[Email params test] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. Email actions have the following configuration properties. diff --git a/docs/management/connectors/images/email-connector.png b/docs/management/connectors/images/email-connector.png index b837fa545a4d1..3cce19021937f 100644 Binary files a/docs/management/connectors/images/email-connector.png and b/docs/management/connectors/images/email-connector.png differ diff --git a/docs/management/connectors/images/email-params-test.png b/docs/management/connectors/images/email-params-test.png index 3745bcd3235e9..bc2a9706e6396 100644 Binary files a/docs/management/connectors/images/email-params-test.png and b/docs/management/connectors/images/email-params-test.png differ diff --git a/examples/controls_example/public/edit_example.tsx b/examples/controls_example/public/edit_example.tsx index f6297befa615c..148867337fedd 100644 --- a/examples/controls_example/public/edit_example.tsx +++ b/examples/controls_example/public/edit_example.tsx @@ -133,7 +133,7 @@ export const EditExample = () => { iconType="plusInCircle" isDisabled={controlGroupAPI === undefined} onClick={() => { - controlGroupAPI!.openAddDataControlFlyout(controlInputTransform); + controlGroupAPI!.openAddDataControlFlyout({ controlInputTransform }); }} > Add control diff --git a/package.json b/package.json index 58b87303d4a02..56fdffa498b05 100644 --- a/package.json +++ b/package.json @@ -470,6 +470,7 @@ "@kbn/maps-plugin": "link:x-pack/plugins/maps", "@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils", "@kbn/ml-date-picker": "link:x-pack/packages/ml/date_picker", + "@kbn/ml-error-utils": "link:x-pack/packages/ml/error_utils", "@kbn/ml-is-defined": "link:x-pack/packages/ml/is_defined", "@kbn/ml-is-populated-object": "link:x-pack/packages/ml/is_populated_object", "@kbn/ml-local-storage": "link:x-pack/packages/ml/local_storage", @@ -672,6 +673,7 @@ "@kbn/upgrade-assistant-plugin": "link:x-pack/plugins/upgrade_assistant", "@kbn/url-drilldown-plugin": "link:x-pack/plugins/drilldowns/url_drilldown", "@kbn/url-forwarding-plugin": "link:src/plugins/url_forwarding", + "@kbn/url-state": "link:packages/kbn-url-state", "@kbn/usage-collection-plugin": "link:src/plugins/usage_collection", "@kbn/usage-collection-test-plugin": "link:test/plugin_functional/plugins/usage_collection", "@kbn/user-profile-components": "link:packages/kbn-user-profile-components", diff --git a/packages/content-management/table_list/index.ts b/packages/content-management/table_list/index.ts index 532b35450d541..9a608b2d6dda3 100644 --- a/packages/content-management/table_list/index.ts +++ b/packages/content-management/table_list/index.ts @@ -8,5 +8,5 @@ export { TableListView, TableListViewProvider, TableListViewKibanaProvider } from './src'; -export type { UserContentCommonSchema } from './src'; +export type { UserContentCommonSchema, RowActions } from './src'; export type { TableListViewKibanaDependencies } from './src/services'; diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list/src/components/table.tsx index 330eb67be4278..3214e7bf00a72 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list/src/components/table.tsx @@ -17,7 +17,9 @@ import { SearchFilterConfig, Direction, Query, + type EuiTableSelectionType, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { useServices } from '../services'; import type { Action } from '../actions'; @@ -26,6 +28,7 @@ import type { Props as TableListViewProps, UserContentCommonSchema, } from '../table_list_view'; +import type { TableItemsRowActions } from '../types'; import { TableSortSelect } from './table_sort_select'; import { TagFilterPanel } from './tag_filter_panel'; import { useTagFilterPanel } from './use_tag_filter_panel'; @@ -51,6 +54,7 @@ interface Props extends State, TagManageme tableColumns: Array>; hasUpdatedAtMetadata: boolean; deleteItems: TableListViewProps['deleteItems']; + tableItemsRowActions: TableItemsRowActions; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; @@ -70,6 +74,7 @@ export function Table({ entityName, entityNamePlural, tagsToTableItemMap, + tableItemsRowActions, deleteItems, tableCaption, onTableChange, @@ -105,13 +110,32 @@ export function Table({ ); }, [deleteItems, dispatch, entityName, entityNamePlural, selectedIds.length]); - const selection = deleteItems - ? { + const selection = useMemo | undefined>(() => { + if (deleteItems) { + return { onSelectionChange: (obj: T[]) => { dispatch({ type: 'onSelectionChange', data: obj }); }, - } - : undefined; + selectable: (obj) => { + const actions = tableItemsRowActions[obj.id]; + return actions?.delete?.enabled !== false; + }, + selectableMessage: (selectable, obj) => { + if (!selectable) { + const actions = tableItemsRowActions[obj.id]; + return ( + actions?.delete?.reason ?? + i18n.translate('contentManagement.tableList.actionsDisabledLabel', { + defaultMessage: 'Actions disabled for this item', + }) + ); + } + return ''; + }, + initialSelected: [], + }; + } + }, [deleteItems, dispatch, tableItemsRowActions]); const { isPopoverOpen, @@ -214,6 +238,7 @@ export function Table({ data-test-subj="itemsInMemTable" rowHeader="attributes.title" tableCaption={tableCaption} + isSelectable /> ); } diff --git a/packages/content-management/table_list/src/index.ts b/packages/content-management/table_list/src/index.ts index df0d1e22bc106..d1e83d7dd2e93 100644 --- a/packages/content-management/table_list/src/index.ts +++ b/packages/content-management/table_list/src/index.ts @@ -15,3 +15,5 @@ export type { } from './table_list_view'; export { TableListViewProvider, TableListViewKibanaProvider } from './services'; + +export type { RowActions } from './types'; diff --git a/packages/content-management/table_list/src/table_list_view.test.tsx b/packages/content-management/table_list/src/table_list_view.test.tsx index 62c83fb5b9454..0245af450fb8a 100644 --- a/packages/content-management/table_list/src/table_list_view.test.tsx +++ b/packages/content-management/table_list/src/table_list_view.test.tsx @@ -1067,4 +1067,109 @@ describe('TableListView', () => { expect(router?.history.location?.search).toBe('?sort=title&sortdir=desc'); }); }); + + describe('row item actions', () => { + const hits: UserContentCommonSchema[] = [ + { + id: '123', + updatedAt: twoDaysAgo.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 1', + description: 'Item 1 description', + }, + references: [], + }, + { + id: '456', + updatedAt: yesterday.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 2', + description: 'Item 2 description', + }, + references: [], + }, + ]; + + const setupTest = async (props?: Partial) => { + let testBed: TestBed | undefined; + const deleteItems = jest.fn(); + await act(async () => { + testBed = await setup({ + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }), + deleteItems, + ...props, + }); + }); + + testBed!.component.update(); + return { testBed: testBed!, deleteItems }; + }; + + test('should allow select items to be deleted', async () => { + const { + testBed: { table, find, exists, component, form }, + deleteItems, + } = await setupTest(); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + + expect(tableCellsValues).toEqual([ + ['', 'Item 2Item 2 description', yesterdayToString], // First empty col is the "checkbox" + ['', 'Item 1Item 1 description', twoDaysAgoToString], + ]); + + const selectedHit = hits[1]; + + expect(exists('deleteSelectedItems')).toBe(false); + act(() => { + // Select the second item + form.selectCheckBox(`checkboxSelectRow-${selectedHit.id}`); + }); + component.update(); + // Delete button is now visible + expect(exists('deleteSelectedItems')).toBe(true); + + // Click delete and validate that confirm modal opens + expect(component.exists('.euiModal--confirmation')).toBe(false); + act(() => { + find('deleteSelectedItems').simulate('click'); + }); + component.update(); + expect(component.exists('.euiModal--confirmation')).toBe(true); + + await act(async () => { + find('confirmModalConfirmButton').simulate('click'); + }); + expect(deleteItems).toHaveBeenCalledWith([selectedHit]); + }); + + test('should allow to disable the "delete" action for a row', async () => { + const reasonMessage = 'This file cannot be deleted.'; + + const { + testBed: { find }, + } = await setupTest({ + rowItemActions: (obj) => { + if (obj.id === hits[1].id) { + return { + delete: { + enabled: false, + reason: reasonMessage, + }, + }; + } + }, + }); + + const firstCheckBox = find(`checkboxSelectRow-${hits[0].id}`); + const secondCheckBox = find(`checkboxSelectRow-${hits[1].id}`); + + expect(firstCheckBox.props().disabled).toBe(false); + expect(secondCheckBox.props().disabled).toBe(true); + // EUI changes the check "title" from "Select this row" to the reason to disable the checkbox + expect(secondCheckBox.props().title).toBe(reasonMessage); + }); + }); }); diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 1612649f80bda..2191a3c9b7eee 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -42,6 +42,7 @@ import { getReducer } from './reducer'; import type { SortColumnField } from './components'; import { useTags } from './use_tags'; import { useInRouterContext, useUrlState } from './use_url_state'; +import { RowActions, TableItemsRowActions } from './types'; interface ContentEditorConfig extends Pick { @@ -67,6 +68,11 @@ export interface Props RowActions | undefined; children?: ReactNode | undefined; findItems( searchQuery: string, @@ -241,6 +247,7 @@ function TableListViewComp({ urlStateEnabled = true, customTableColumn, emptyPrompt, + rowItemActions, findItems, createItem, editItem, @@ -580,6 +587,15 @@ function TableListViewComp({ return selectedIds.map((selectedId) => itemsById[selectedId]); }, [selectedIds, itemsById]); + const tableItemsRowActions = useMemo(() => { + return items.reduce((acc, item) => { + return { + ...acc, + [item.id]: rowItemActions ? rowItemActions(item) : undefined, + }; + }, {}); + }, [items, rowItemActions]); + // ------------ // Callbacks // ------------ @@ -854,6 +870,20 @@ function TableListViewComp({ }; }, []); + const PageTemplate = useMemo(() => { + return withoutPageTemplateWrapper + ? ((({ + children: _children, + 'data-test-subj': dataTestSubj, + }: { + children: React.ReactNode; + ['data-test-subj']?: string; + }) => ( +
{_children}
+ )) as unknown as typeof KibanaPageTemplate) + : KibanaPageTemplate; + }, [withoutPageTemplateWrapper]); + // ------------ // Render // ------------ @@ -861,10 +891,6 @@ function TableListViewComp({ return null; } - const PageTemplate = withoutPageTemplateWrapper - ? (React.Fragment as unknown as typeof KibanaPageTemplate) - : KibanaPageTemplate; - if (!showFetchError && hasNoItems) { return ( @@ -929,6 +955,7 @@ function TableListViewComp({ tagsToTableItemMap={tagsToTableItemMap} deleteItems={deleteItems} tableCaption={tableListTitle} + tableItemsRowActions={tableItemsRowActions} onTableChange={onTableChange} onTableSearchChange={onTableSearchChange} onSortChange={onSortChange} diff --git a/packages/content-management/table_list/src/types.ts b/packages/content-management/table_list/src/types.ts index 0e716e6d59cf3..c8e734a289451 100644 --- a/packages/content-management/table_list/src/types.ts +++ b/packages/content-management/table_list/src/types.ts @@ -12,3 +12,16 @@ export interface Tag { description: string; color: string; } + +export type TableRowAction = 'delete'; + +export type RowActions = { + [action in TableRowAction]?: { + enabled: boolean; + reason?: string; + }; +}; + +export interface TableItemsRowActions { + [id: string]: RowActions | undefined; +} diff --git a/packages/kbn-cell-actions/README.md b/packages/kbn-cell-actions/README.md index 7cacff6becda6..e1ae1a73a8369 100644 --- a/packages/kbn-cell-actions/README.md +++ b/packages/kbn-cell-actions/README.md @@ -6,7 +6,7 @@ Example: ```JSX [...] - + Hover me diff --git a/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx b/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx index 657dcc93416b8..49f812cd9005a 100644 --- a/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx +++ b/packages/kbn-cell-actions/src/__stories__/cell_actions.stories.tsx @@ -50,8 +50,8 @@ export const DefaultWithControls = CellActionsTemplate.bind({}); DefaultWithControls.argTypes = { mode: { - options: [CellActionsMode.HOVER, CellActionsMode.INLINE], - defaultValue: CellActionsMode.HOVER, + options: [CellActionsMode.HOVER_DOWN, CellActionsMode.INLINE], + defaultValue: CellActionsMode.HOVER_DOWN, control: { type: 'radio', }, @@ -72,8 +72,14 @@ export const CellActionInline = ({}: {}) => ( ); -export const CellActionHoverPopup = ({}: {}) => ( - +export const CellActionHoverPopoverDown = ({}: {}) => ( + + Hover me + +); + +export const CellActionHoverPopoverRight = ({}: {}) => ( + Hover me ); diff --git a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx index ec266889fe255..36d0482ebf84d 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.test.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.test.tsx @@ -15,6 +15,11 @@ import { CellActionsProvider } from '../context/cell_actions_context'; const TRIGGER_ID = 'test-trigger-id'; const FIELD = { name: 'name', value: '123', type: 'text' }; +jest.mock('./hover_actions_popover', () => ({ + HoverActionsPopover: jest.fn((props) => ( + {props.anchorPosition} + )), +})); describe('CellActions', () => { it('renders', async () => { const getActionsPromise = Promise.resolve([]); @@ -54,13 +59,33 @@ describe('CellActions', () => { expect(queryByTestId('inlineActions')).toBeInTheDocument(); }); - it('renders HoverActionsPopover when mode is HOVER', async () => { + it('renders HoverActionsPopover when mode is HOVER_DOWN', async () => { const getActionsPromise = Promise.resolve([]); const getActions = () => getActionsPromise; - const { queryByTestId } = render( + const { getByTestId } = render( + + + Field value + + + ); + + await act(async () => { + await getActionsPromise; + }); + + expect(getByTestId('hoverActionsPopover')).toBeInTheDocument(); + expect(getByTestId('hoverActionsPopover')).toHaveTextContent('downCenter'); + }); + + it('renders HoverActionsPopover when mode is HOVER_RIGHT', async () => { + const getActionsPromise = Promise.resolve([]); + const getActions = () => getActionsPromise; + + const { getByTestId } = render( - + Field value @@ -70,6 +95,7 @@ describe('CellActions', () => { await getActionsPromise; }); - expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument(); + expect(getByTestId('hoverActionsPopover')).toBeInTheDocument(); + expect(getByTestId('hoverActionsPopover')).toHaveTextContent('rightCenter'); }); }); diff --git a/packages/kbn-cell-actions/src/components/cell_actions.tsx b/packages/kbn-cell-actions/src/components/cell_actions.tsx index f6d3288ed6f7d..df6f957575c20 100644 --- a/packages/kbn-cell-actions/src/components/cell_actions.tsx +++ b/packages/kbn-cell-actions/src/components/cell_actions.tsx @@ -36,11 +36,17 @@ export const CellActions: React.FC = ({ [field, triggerId, metadata] ); + const anchorPosition = useMemo( + () => (mode === CellActionsMode.HOVER_DOWN ? 'downCenter' : 'rightCenter'), + [mode] + ); + const dataTestSubj = `cellActions-renderContent-${field.name}`; - if (mode === CellActionsMode.HOVER) { + if (mode === CellActionsMode.HOVER_DOWN || mode === CellActionsMode.HOVER_RIGHT) { return (
= ({ {children} {}, + actions: [], + button: , +}; describe('ExtraActionsPopOver', () => { it('renders', () => { - const { queryByTestId } = render( - {}} - actions={[]} - button={} - /> - ); + const { queryByTestId } = render(); expect(queryByTestId('extraActionsPopOver')).toBeInTheDocument(); }); @@ -33,11 +33,10 @@ describe('ExtraActionsPopOver', () => { const action = { ...makeAction('test-action'), execute: executeAction }; const { getByLabelText } = render( } /> ); @@ -56,13 +55,7 @@ describe('ExtraActionsPopOverWithAnchor', () => { it('renders', () => { const { queryByTestId } = render( - {}} - actions={[]} - anchorRef={{ current: anchorElement }} - /> + ); expect(queryByTestId('extraActionsPopOverWithAnchor')).toBeInTheDocument(); @@ -74,7 +67,7 @@ describe('ExtraActionsPopOverWithAnchor', () => { const action = { ...makeAction('test-action'), execute: executeAction }; const { getByLabelText } = render( void; @@ -32,6 +33,7 @@ interface ActionsPopOverProps { } export const ExtraActionsPopOver: React.FC = ({ + anchorPosition, actions, actionContext, isOpen, @@ -43,7 +45,7 @@ export const ExtraActionsPopOver: React.FC = ({ isOpen={isOpen} closePopover={closePopOver} panelPaddingSize="xs" - anchorPosition={'downCenter'} + anchorPosition={anchorPosition} hasArrow repositionOnScroll ownFocus @@ -59,11 +61,15 @@ export const ExtraActionsPopOver: React.FC = ({ ); interface ExtraActionsPopOverWithAnchorProps - extends Pick { + extends Pick< + ActionsPopOverProps, + 'anchorPosition' | 'actionContext' | 'closePopOver' | 'isOpen' | 'actions' + > { anchorRef: React.RefObject; } export const ExtraActionsPopOverWithAnchor = ({ + anchorPosition, anchorRef, actionContext, isOpen, @@ -77,7 +83,7 @@ export const ExtraActionsPopOverWithAnchor = ({ isOpen={isOpen} closePopover={closePopOver} panelPaddingSize="xs" - anchorPosition={'downCenter'} + anchorPosition={anchorPosition} hasArrow={false} repositionOnScroll ownFocus diff --git a/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx b/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx index b30ca63e52ec0..de68ccd6fca51 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.test.tsx @@ -13,11 +13,17 @@ import { makeAction } from '../mocks/helpers'; import { CellActionExecutionContext } from '../types'; import { HoverActionsPopover } from './hover_actions_popover'; -describe('HoverActionsPopover', () => { - const actionContext = { +const defaultProps = { + anchorPosition: 'rightCenter' as const, + disabledActionTypes: [], + visibleCellActions: 4, + actionContext: { trigger: { id: 'triggerId' }, field: { name: 'fieldName' }, - } as CellActionExecutionContext; + } as CellActionExecutionContext, + showActionTooltips: false, +}; +describe('HoverActionsPopover', () => { const TestComponent = () => ; jest.useFakeTimers(); @@ -25,13 +31,7 @@ describe('HoverActionsPopover', () => { const getActions = () => Promise.resolve([]); const { queryByTestId } = render( - + ); expect(queryByTestId('hoverActionsPopover')).toBeInTheDocument(); @@ -44,12 +44,7 @@ describe('HoverActionsPopover', () => { const { queryByLabelText, getByTestId } = render( - + @@ -70,12 +65,7 @@ describe('HoverActionsPopover', () => { const { queryByLabelText, getByTestId } = render( - + @@ -101,12 +91,7 @@ describe('HoverActionsPopover', () => { const { getByTestId } = render( - + @@ -127,12 +112,7 @@ describe('HoverActionsPopover', () => { const { getByTestId, getByLabelText } = render( - + @@ -162,12 +142,7 @@ describe('HoverActionsPopover', () => { const { getByTestId, queryByLabelText } = render( - + @@ -191,6 +166,44 @@ describe('HoverActionsPopover', () => { expect(queryByLabelText('test-action-2')).toBeInTheDocument(); expect(queryByLabelText('test-action-3')).toBeInTheDocument(); }); + it('does not add css positioning when anchorPosition = downCenter', async () => { + const getActionsPromise = Promise.resolve([makeAction('test-action')]); + const getActions = () => getActionsPromise; + + const { getByLabelText, getByTestId } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + expect(Object.values(getByLabelText('Actions').style).includes('margin-top')).toEqual(false); + }); + it('adds css positioning when anchorPosition = rightCenter', async () => { + const getActionsPromise = Promise.resolve([makeAction('test-action')]); + const getActions = () => getActionsPromise; + + const { getByLabelText, getByTestId } = render( + + + + + + ); + + await hoverElement(getByTestId('test-component'), async () => { + await getActionsPromise; + jest.runAllTimers(); + }); + + expect(Object.values(getByLabelText('Actions').style).includes('margin-top')).toEqual(true); + }); }); const hoverElement = async (element: Element, waitForChange: () => Promise) => { diff --git a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx index 62ea3e766eaf7..dd087aa755307 100644 --- a/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx +++ b/packages/kbn-cell-actions/src/components/hover_actions_popover.tsx @@ -36,6 +36,7 @@ const hoverContentWrapperCSS = css` const HOVER_INTENT_DELAY = 100; // ms interface Props { + anchorPosition: 'downCenter' | 'rightCenter'; children: React.ReactNode; visibleCellActions: number; actionContext: CellActionExecutionContext; @@ -44,6 +45,7 @@ interface Props { } export const HoverActionsPopover: React.FC = ({ + anchorPosition, children, visibleCellActions, actionContext, @@ -115,12 +117,17 @@ export const HoverActionsPopover: React.FC = ({ ); }, [onMouseEnter, closeExtraActions, children]); + const panelStyle = useMemo( + () => (anchorPosition === 'rightCenter' ? { marginTop: euiThemeVars.euiSizeS } : {}), + [anchorPosition] + ); + return ( <>
= ({
{ - const actionContext = { trigger: { id: 'triggerId' } } as CellActionExecutionContext; it('renders', async () => { const getActionsPromise = Promise.resolve([]); const getActions = () => getActionsPromise; const { queryByTestId } = render( - + ); @@ -47,12 +48,7 @@ describe('InlineActions', () => { const getActions = () => getActionsPromise; const { queryAllByRole } = render( - + ); diff --git a/packages/kbn-cell-actions/src/components/inline_actions.tsx b/packages/kbn-cell-actions/src/components/inline_actions.tsx index cd2cfcc88edf0..c46fb5c57af76 100644 --- a/packages/kbn-cell-actions/src/components/inline_actions.tsx +++ b/packages/kbn-cell-actions/src/components/inline_actions.tsx @@ -17,6 +17,7 @@ import { useLoadActions } from '../hooks/use_load_actions'; interface InlineActionsProps { actionContext: CellActionExecutionContext; + anchorPosition: 'rightCenter' | 'downCenter'; showActionTooltips: boolean; visibleCellActions: number; disabledActionTypes: string[]; @@ -24,6 +25,7 @@ interface InlineActionsProps { export const InlineActions: React.FC = ({ actionContext, + anchorPosition, showActionTooltips, visibleCellActions, disabledActionTypes, @@ -47,10 +49,9 @@ export const InlineActions: React.FC = ({ data-test-subj="inlineActions" className={`inlineActions ${isPopoverOpen ? 'inlineActions-popoverOpen' : ''}`} > - {visibleActions.map((action, index) => ( - + {visibleActions.map((action) => ( + = ({ { licenseManagement: `${ENTERPRISE_SEARCH_DOCS}license-management.html`, machineLearningStart: `${ENTERPRISE_SEARCH_DOCS}machine-learning-start.html`, mailService: `${ENTERPRISE_SEARCH_DOCS}mailer-configuration.html`, + mlDocumentEnrichment: `${ENTERPRISE_SEARCH_DOCS}document-enrichment.html`, start: `${ENTERPRISE_SEARCH_DOCS}start.html`, syncRules: `${ENTERPRISE_SEARCH_DOCS}sync-rules.html`, troubleshootSetup: `${ENTERPRISE_SEARCH_DOCS}troubleshoot-setup.html`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 92dc64e916644..efe5e95f238d0 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -138,6 +138,7 @@ export interface DocLinks { readonly licenseManagement: string; readonly machineLearningStart: string; readonly mailService: string; + readonly mlDocumentEnrichment: string; readonly start: string; readonly syncRules: string; readonly troubleshootSetup: string; diff --git a/packages/kbn-expandable-flyout/index.ts b/packages/kbn-expandable-flyout/index.ts index e2ce15d85a399..cc423eb275090 100644 --- a/packages/kbn-expandable-flyout/index.ts +++ b/packages/kbn-expandable-flyout/index.ts @@ -7,7 +7,13 @@ */ export { ExpandableFlyout } from './src'; -export { ExpandableFlyoutProvider, useExpandableFlyoutContext } from './src/context'; +export { + ExpandableFlyoutProvider, + useExpandableFlyoutContext, + type ExpandableFlyoutContext, +} from './src/context'; + +export type { ExpandableFlyoutApi } from './src/context'; export type { ExpandableFlyoutProps } from './src'; export type { FlyoutPanel } from './src/types'; diff --git a/packages/kbn-expandable-flyout/src/context.tsx b/packages/kbn-expandable-flyout/src/context.tsx index 89e8210e9578f..b7ad721a2b9fd 100644 --- a/packages/kbn-expandable-flyout/src/context.tsx +++ b/packages/kbn-expandable-flyout/src/context.tsx @@ -6,7 +6,15 @@ * Side Public License, v 1. */ -import React, { createContext, useCallback, useContext, useMemo, useReducer } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useReducer, +} from 'react'; import { ActionType } from './actions'; import { reducer, State } from './reducer'; import type { FlyoutPanel } from './types'; @@ -59,19 +67,44 @@ export const ExpandableFlyoutContext = createContext & { + getState: () => State; +}; + export interface ExpandableFlyoutProviderProps { /** * React children */ children: React.ReactNode; + /** + * Triggered whenever flyout state changes. You can use it to store it's state somewhere for instance. + */ + onChanges?: (state: State) => void; + /** + * Triggered whenever flyout is closed. This is independent from the onChanges above. + */ + onClosePanels?: () => void; } /** * Wrap your plugin with this context for the ExpandableFlyout React component. */ -export const ExpandableFlyoutProvider = ({ children }: ExpandableFlyoutProviderProps) => { +export const ExpandableFlyoutProvider = React.forwardRef< + ExpandableFlyoutApi, + ExpandableFlyoutProviderProps +>(({ children, onChanges = () => {}, onClosePanels = () => {} }, ref) => { const [state, dispatch] = useReducer(reducer, initialState); + useEffect(() => { + const closed = !state.right; + if (closed) { + // manual close is singalled via separate callback + return; + } + + onChanges(state); + }, [state, onChanges]); + const openPanels = useCallback( ({ right, @@ -87,40 +120,45 @@ export const ExpandableFlyoutProvider = ({ children }: ExpandableFlyoutProviderP const openRightPanel = useCallback( (panel: FlyoutPanel) => dispatch({ type: ActionType.openRightPanel, payload: panel }), - [dispatch] + [] ); const openLeftPanel = useCallback( (panel: FlyoutPanel) => dispatch({ type: ActionType.openLeftPanel, payload: panel }), - [dispatch] + [] ); const openPreviewPanel = useCallback( (panel: FlyoutPanel) => dispatch({ type: ActionType.openPreviewPanel, payload: panel }), - [dispatch] + [] ); - const closeRightPanel = useCallback( - () => dispatch({ type: ActionType.closeRightPanel }), - [dispatch] - ); + const closeRightPanel = useCallback(() => dispatch({ type: ActionType.closeRightPanel }), []); - const closeLeftPanel = useCallback( - () => dispatch({ type: ActionType.closeLeftPanel }), - [dispatch] - ); + const closeLeftPanel = useCallback(() => dispatch({ type: ActionType.closeLeftPanel }), []); - const closePreviewPanel = useCallback( - () => dispatch({ type: ActionType.closePreviewPanel }), - [dispatch] - ); + const closePreviewPanel = useCallback(() => dispatch({ type: ActionType.closePreviewPanel }), []); const previousPreviewPanel = useCallback( () => dispatch({ type: ActionType.previousPreviewPanel }), - [dispatch] + [] ); - const closePanels = useCallback(() => dispatch({ type: ActionType.closeFlyout }), [dispatch]); + const closePanels = useCallback(() => { + dispatch({ type: ActionType.closeFlyout }); + onClosePanels(); + }, [onClosePanels]); + + useImperativeHandle( + ref, + () => { + return { + openFlyout: openPanels, + getState: () => state, + }; + }, + [openPanels, state] + ); const contextValue = useMemo( () => ({ @@ -154,7 +192,7 @@ export const ExpandableFlyoutProvider = ({ children }: ExpandableFlyoutProviderP {children} ); -}; +}); /** * Retrieve context's properties diff --git a/packages/kbn-expandable-flyout/src/reducer.ts b/packages/kbn-expandable-flyout/src/reducer.ts index 4901eccfc6bb4..bb0ff125f546e 100644 --- a/packages/kbn-expandable-flyout/src/reducer.ts +++ b/packages/kbn-expandable-flyout/src/reducer.ts @@ -105,5 +105,8 @@ export function reducer(state: State, action: Action) { preview: [], }; } + + default: + return state; } } diff --git a/packages/kbn-io-ts-utils/index.ts b/packages/kbn-io-ts-utils/index.ts index e52e4d429829e..e8e6da2e7b59e 100644 --- a/packages/kbn-io-ts-utils/index.ts +++ b/packages/kbn-io-ts-utils/index.ts @@ -7,7 +7,7 @@ */ export type { IndexPatternType } from './src/index_pattern_rt'; -export type { NonEmptyStringBrand } from './src/non_empty_string_rt'; +export type { NonEmptyString, NonEmptyStringBrand } from './src/non_empty_string_rt'; export { deepExactRt } from './src/deep_exact_rt'; export { indexPatternRt } from './src/index_pattern_rt'; diff --git a/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap b/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap index 871e3ea311cd6..b72c08b8d74ac 100644 --- a/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap +++ b/packages/kbn-securitysolution-exception-list-components/src/list_header/__snapshots__/list_header.test.tsx.snap @@ -1876,6 +1876,27 @@ Object { data-test-subj="RightSideMenuItemsMenuActionsActionItem2" disabled="" type="button" + > + + + + Duplicate exception list + + + + + + +
+ ); +} +``` + +In this example, the count state is synced to the URL using the `useSyncToUrl` hook. +Whenever the count state changes, the hook will update the URL with the new value. +When the user copies the updated url or refreshes the page, `restore` function will be called to update the count state. \ No newline at end of file diff --git a/packages/kbn-url-state/index.test.ts b/packages/kbn-url-state/index.test.ts new file mode 100644 index 0000000000000..33dc285e100e5 --- /dev/null +++ b/packages/kbn-url-state/index.test.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook, act } from '@testing-library/react-hooks'; +import { useSyncToUrl } from '.'; +import { encode } from '@kbn/rison'; + +describe('useSyncToUrl', () => { + let originalLocation: Location; + let originalHistory: History; + + beforeEach(() => { + originalLocation = window.location; + originalHistory = window.history; + delete (window as any).location; + delete (window as any).history; + + window.location = { + ...originalLocation, + search: '', + }; + window.history = { + ...originalHistory, + replaceState: jest.fn(), + }; + }); + + afterEach(() => { + window.location = originalLocation; + window.history = originalHistory; + }); + + it('should restore the value from the query string on mount', () => { + const key = 'testKey'; + const restoredValue = { test: 'value' }; + const encodedValue = encode(restoredValue); + const restore = jest.fn(); + + window.location.search = `?${key}=${encodedValue}`; + + renderHook(() => useSyncToUrl(key, restore)); + + expect(restore).toHaveBeenCalledWith(restoredValue); + }); + + it('should sync the value to the query string', () => { + const key = 'testKey'; + const valueToSerialize = { test: 'value' }; + + const { result } = renderHook(() => useSyncToUrl(key, jest.fn())); + + act(() => { + result.current(valueToSerialize); + }); + + expect(window.history.replaceState).toHaveBeenCalledWith( + { path: expect.any(String) }, + '', + '/?testKey=%28test%3Avalue%29' + ); + }); + + it('should clear the value from the query string on unmount', () => { + const key = 'testKey'; + + const { unmount } = renderHook(() => useSyncToUrl(key, jest.fn())); + + act(() => { + unmount(); + }); + + expect(window.history.replaceState).toHaveBeenCalledWith( + { path: expect.any(String) }, + '', + expect.any(String) + ); + }); + + it('should clear the value from the query string when history back or forward is pressed', () => { + const key = 'testKey'; + const restore = jest.fn(); + + renderHook(() => useSyncToUrl(key, restore, true)); + + act(() => { + window.dispatchEvent(new Event('popstate')); + }); + + expect(window.history.replaceState).toHaveBeenCalledTimes(1); + expect(window.history.replaceState).toHaveBeenCalledWith( + { path: expect.any(String) }, + '', + '/?' + ); + }); +}); diff --git a/packages/kbn-url-state/index.ts b/packages/kbn-url-state/index.ts new file mode 100644 index 0000000000000..73568222fb4c0 --- /dev/null +++ b/packages/kbn-url-state/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { useSyncToUrl } from './use_sync_to_url'; diff --git a/packages/kbn-url-state/jest.config.js b/packages/kbn-url-state/jest.config.js new file mode 100644 index 0000000000000..256a51239206c --- /dev/null +++ b/packages/kbn-url-state/jest.config.js @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../..', + roots: ['/packages/kbn-url-state'], +}; diff --git a/packages/kbn-url-state/kibana.jsonc b/packages/kbn-url-state/kibana.jsonc new file mode 100644 index 0000000000000..b0ab56d6af8b7 --- /dev/null +++ b/packages/kbn-url-state/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/url-state", + "owner": "@elastic/security-threat-hunting-investigations" +} diff --git a/packages/kbn-url-state/package.json b/packages/kbn-url-state/package.json new file mode 100644 index 0000000000000..2cd753f16b872 --- /dev/null +++ b/packages/kbn-url-state/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/url-state", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-url-state/tsconfig.json b/packages/kbn-url-state/tsconfig.json new file mode 100644 index 0000000000000..3bd03b7f37b84 --- /dev/null +++ b/packages/kbn-url-state/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/rison", + ] +} diff --git a/packages/kbn-url-state/use_sync_to_url.ts b/packages/kbn-url-state/use_sync_to_url.ts new file mode 100644 index 0000000000000..e38f9bc688a8b --- /dev/null +++ b/packages/kbn-url-state/use_sync_to_url.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useCallback, useEffect } from 'react'; +import { encode, decode } from '@kbn/rison'; + +// https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event +const POPSTATE_EVENT = 'popstate' as const; + +/** + * Sync any object with browser query string using @knb/rison + * @param key query string param to use + * @param restore use this to handle restored state + * @param cleanupOnHistoryNavigation use history events to cleanup state on back / forward naviation. true by default + */ +export const useSyncToUrl = ( + key: string, + restore: (data: TValueToSerialize) => void, + cleanupOnHistoryNavigation = true +) => { + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const param = params.get(key); + + if (!param) { + return; + } + + const decodedQuery = decode(param); + + if (!decodedQuery) { + return; + } + + // Only restore the value if it is not falsy + restore(decodedQuery as unknown as TValueToSerialize); + }, [key, restore]); + + /** + * Synces value with the url state, under specified key. If payload is undefined, the value will be removed from the query string althogether. + */ + const syncValueToQueryString = useCallback( + (valueToSerialize?: TValueToSerialize) => { + const searchParams = new URLSearchParams(window.location.search); + + if (valueToSerialize) { + const serializedPayload = encode(valueToSerialize); + searchParams.set(key, serializedPayload); + } else { + searchParams.delete(key); + } + + const newSearch = searchParams.toString(); + + // Update query string without unnecessary re-render + const newUrl = `${window.location.pathname}?${newSearch}`; + window.history.replaceState({ path: newUrl }, '', newUrl); + }, + [key] + ); + + // Clear remove state from the url on unmount / when history back or forward is pressed + useEffect(() => { + const clearState = () => { + syncValueToQueryString(undefined); + }; + + if (cleanupOnHistoryNavigation) { + window.addEventListener(POPSTATE_EVENT, clearState); + } + + return () => { + clearState(); + + if (cleanupOnHistoryNavigation) { + window.removeEventListener(POPSTATE_EVENT, clearState); + } + }; + }, [cleanupOnHistoryNavigation, syncValueToQueryString]); + + return syncValueToQueryString; +}; diff --git a/packages/shared-ux/file/types/base_file_client.ts b/packages/shared-ux/file/types/base_file_client.ts index 4a00f2de00516..52d1ca09fd170 100644 --- a/packages/shared-ux/file/types/base_file_client.ts +++ b/packages/shared-ux/file/types/base_file_client.ts @@ -27,6 +27,7 @@ export interface BaseFilesClient { find: ( args: { kind?: string | string[]; + kindToExclude?: string | string[]; status?: string | string[]; extension?: string | string[]; name?: string | string[]; diff --git a/packages/shared-ux/file/types/index.ts b/packages/shared-ux/file/types/index.ts index 4c49124f7149f..86b9e47fdab43 100644 --- a/packages/shared-ux/file/types/index.ts +++ b/packages/shared-ux/file/types/index.ts @@ -250,6 +250,22 @@ export interface FileKindBrowser extends FileKindBase { * @default 4MiB */ maxSizeBytes?: number; + /** + * Allowed actions that can be done in the File Management UI. If not provided, all actions are allowed + * + */ + managementUiActions?: { + /** Allow files to be listed in management UI */ + list?: { + enabled: boolean; + }; + /** Allow files to be deleted in management UI */ + delete?: { + enabled: boolean; + /** If delete is not enabled in management UI, specify the reason (will appear in a tooltip). */ + reason?: string; + }; + }; } /** diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index da762f8f55725..c612ef05d4b7a 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -37,6 +37,7 @@ export const storybookAliases = { expression_shape: 'src/plugins/expression_shape/.storybook', expression_tagcloud: 'src/plugins/chart_expressions/expression_tagcloud/.storybook', fleet: 'x-pack/plugins/fleet/.storybook', + grouping: 'packages/kbn-securitysolution-grouping/.storybook', home: 'src/plugins/home/.storybook', infra: 'x-pack/plugins/infra/.storybook', kibana_react: 'src/plugins/kibana_react/.storybook', diff --git a/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx b/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx index bf60193432488..695eaa42e064d 100644 --- a/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx +++ b/src/plugins/controls/public/control_group/editor/open_add_data_control_flyout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public'; import { ControlGroupContainer, @@ -32,8 +33,12 @@ import { DataControlInput, OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '.. export function openAddDataControlFlyout( this: ControlGroupContainer, - controlInputTransform?: ControlInputTransform + options?: { + controlInputTransform?: ControlInputTransform; + onSave?: (id: string) => void; + } ) { + const { controlInputTransform, onSave } = options || {}; const { overlays: { openFlyout, openConfirm }, controls: { getControlFactory }, @@ -71,7 +76,7 @@ export function openAddDataControlFlyout( updateTitle={(newTitle) => (controlInput.title = newTitle)} updateWidth={(defaultControlWidth) => this.updateInput({ defaultControlWidth })} updateGrow={(defaultControlGrow: boolean) => this.updateInput({ defaultControlGrow })} - onSave={(type) => { + onSave={async (type) => { this.closeAllFlyouts(); if (!type) { return; @@ -86,17 +91,28 @@ export function openAddDataControlFlyout( controlInput = controlInputTransform({ ...controlInput }, type); } - if (type === OPTIONS_LIST_CONTROL) { - this.addOptionsListControl(controlInput as AddOptionsListControlProps); - return; - } + let newControl; - if (type === RANGE_SLIDER_CONTROL) { - this.addRangeSliderControl(controlInput as AddRangeSliderControlProps); - return; + switch (type) { + case OPTIONS_LIST_CONTROL: + newControl = await this.addOptionsListControl( + controlInput as AddOptionsListControlProps + ); + break; + case RANGE_SLIDER_CONTROL: + newControl = await this.addRangeSliderControl( + controlInput as AddRangeSliderControlProps + ); + break; + default: + newControl = await this.addDataControlFromField( + controlInput as AddDataControlProps + ); } - this.addDataControlFromField(controlInput as AddDataControlProps); + if (onSave && !isErrorEmbeddable(newControl)) { + onSave(newControl.id); + } }} onCancel={onCancel} onTypeEditorChange={(partialInput) => diff --git a/src/plugins/controls/public/services/options_list/options_list_service.ts b/src/plugins/controls/public/services/options_list/options_list_service.ts index 7152d190c997d..4ad0ecddb80a8 100644 --- a/src/plugins/controls/public/services/options_list/options_list_service.ts +++ b/src/plugins/controls/public/services/options_list/options_list_service.ts @@ -99,7 +99,7 @@ class OptionsListService implements ControlsOptionsListService { private cachedAllowExpensiveQueries = memoize(async () => { const { allowExpensiveQueries } = await this.http.get<{ allowExpensiveQueries: boolean; - }>('/api/kibana/controls/optionsList/getClusterSettings'); + }>('/api/kibana/controls/optionsList/getExpensiveQueriesSetting'); return allowExpensiveQueries; }); diff --git a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts index f3b4ea598b886..c757a56950da3 100644 --- a/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts +++ b/src/plugins/controls/server/options_list/options_list_cluster_settings_route.ts @@ -8,18 +8,21 @@ import { getKbnServerError, reportServerError } from '@kbn/kibana-utils-plugin/server'; import { CoreSetup } from '@kbn/core/server'; -import { errors } from '@elastic/elasticsearch'; export const setupOptionsListClusterSettingsRoute = ({ http }: CoreSetup) => { const router = http.createRouter(); router.get( { - path: '/api/kibana/controls/optionsList/getClusterSettings', + path: '/api/kibana/controls/optionsList/getExpensiveQueriesSetting', validate: false, }, async (context, _, response) => { try { - const esClient = (await context.core).elasticsearch.client.asCurrentUser; + /** + * using internal user here because in many cases the logged in user will not have the monitor permission required + * to check cluster settings. This endpoint does not take a query, params, or a body, so there is no chance of leaking info. + */ + const esClient = (await context.core).elasticsearch.client.asInternalUser; const settings = await esClient.cluster.getSettings({ include_defaults: true, filter_path: '**.allow_expensive_queries', @@ -40,17 +43,6 @@ export const setupOptionsListClusterSettingsRoute = ({ http }: CoreSetup) => { }, }); } catch (e) { - if (e instanceof errors.ResponseError && e.body.error.type === 'security_exception') { - /** - * in cases where the user does not have the 'monitor' permission this check will fail. In these cases, we will - * fall back to assume that the allowExpensiveQueries setting is on, because it defaults to true. - */ - return response.ok({ - body: { - allowExpensiveQueries: true, - }, - }); - } const kbnErr = getKbnServerError(e); return reportServerError(response, kbnErr); } diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx index 482553e2f002f..228db7138fd54 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx @@ -95,6 +95,7 @@ export class ClonePanelAction implements Action { height: panelToClone.gridData.h, currentPanels: dashboard.getInput().panels, placeBesideId: panelToClone.explicitInput.id, + scrollToPanel: true, } as IPanelPlacementBesideArgs ); } diff --git a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx index 0d3dd592dcc34..4e98a6dd31024 100644 --- a/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/expand_panel_action.tsx @@ -64,5 +64,9 @@ export class ExpandPanelAction implements Action { } const newValue = isExpanded(embeddable) ? undefined : embeddable.id; (embeddable.parent as DashboardContainer).setExpandedPanelId(newValue); + + if (!newValue) { + (embeddable.parent as DashboardContainer).setScrollToPanelId(embeddable.id); + } } } diff --git a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx index d5d5c439a4745..14067f0b6aa68 100644 --- a/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/replace_panel_flyout.tsx @@ -21,6 +21,7 @@ import { Toast } from '@kbn/core/public'; import { DashboardPanelState } from '../../common'; import { pluginServices } from '../services/plugin_services'; import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings'; +import { DashboardContainer } from '../dashboard_container'; interface Props { container: IContainer; @@ -81,8 +82,8 @@ export class ReplacePanelFlyout extends React.Component { } as DashboardPanelState, }, }); - container.reload(); + (container as DashboardContainer).setHighlightPanelId(id); this.showToast(name); this.props.onClose(); }; diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx index 6cef7e858b165..e7c7daa2bcc27 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_data_control_button.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { ControlGroupContainer } from '@kbn/controls-plugin/public'; import { getAddControlButtonTitle } from '../../_dashboard_app_strings'; +import { useDashboardAPI } from '../../dashboard_app'; interface Props { closePopover: () => void; @@ -17,6 +18,11 @@ interface Props { } export const AddDataControlButton = ({ closePopover, controlGroup, ...rest }: Props) => { + const dashboard = useDashboardAPI(); + const onSave = () => { + dashboard.scrollToTop(); + }; + return ( { - controlGroup.openAddDataControlFlyout(); + controlGroup.openAddDataControlFlyout({ onSave }); closePopover(); }} > diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx index 8283144e1c155..cbd514be8ba13 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/controls_toolbar_button/add_time_slider_control_button.tsx @@ -13,6 +13,7 @@ import { getAddTimeSliderControlButtonTitle, getOnlyOneTimeSliderControlMsg, } from '../../_dashboard_app_strings'; +import { useDashboardAPI } from '../../dashboard_app'; interface Props { closePopover: () => void; @@ -21,6 +22,7 @@ interface Props { export const AddTimeSliderControlButton = ({ closePopover, controlGroup, ...rest }: Props) => { const [hasTimeSliderControl, setHasTimeSliderControl] = useState(false); + const dashboard = useDashboardAPI(); useEffect(() => { const subscription = controlGroup.getInput$().subscribe(() => { @@ -42,8 +44,9 @@ export const AddTimeSliderControlButton = ({ closePopover, controlGroup, ...rest { - controlGroup.addTimeSliderControl(); + onClick={async () => { + await controlGroup.addTimeSliderControl(); + dashboard.scrollToTop(); closePopover(); }} data-test-subj="controls-create-timeslider-button" diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 03b609ae99736..708af176d785d 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -110,6 +110,8 @@ export function DashboardEditingToolbar() { const newEmbeddable = await dashboard.addNewEmbeddable(embeddableFactory.type, explicitInput); if (newEmbeddable) { + dashboard.setScrollToPanelId(newEmbeddable.id); + dashboard.setHighlightPanelId(newEmbeddable.id); toasts.addSuccess({ title: dashboardReplacePanelActionStrings.getSuccessMessage(newEmbeddable.getTitle()), 'data-test-subj': 'addEmbeddableToDashboardSuccess', diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss index 7e9529a90be8b..cc96c816ce8b7 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_grid.scss @@ -36,10 +36,13 @@ } /** - * When a single panel is expanded, all the other panels are hidden in the grid. + * When a single panel is expanded, all the other panels moved offscreen. + * Shifting the rendered panels offscreen prevents a quick flash when redrawing the panels on minimize */ .dshDashboardGrid__item--hidden { - display: none; + position: absolute; + top: -9999px; + left: -9999px; } /** @@ -53,11 +56,12 @@ * 1. We need to mark this as important because react grid layout sets the width and height of the panels inline. */ .dshDashboardGrid__item--expanded { + position: absolute; height: 100% !important; /* 1 */ width: 100% !important; /* 1 */ top: 0 !important; /* 1 */ left: 0 !important; /* 1 */ - transform: translate(0, 0) !important; /* 1 */ + transform: none !important; padding: $euiSizeS; // Altered panel styles can be found in ../panel diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx index b840fcd408977..0055e24685b89 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx @@ -12,7 +12,7 @@ import 'react-grid-layout/css/styles.css'; import { pick } from 'lodash'; import classNames from 'classnames'; import { useEffectOnce } from 'react-use/lib'; -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout'; import { ViewMode } from '@kbn/embeddable-plugin/public'; @@ -38,6 +38,15 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { setTimeout(() => setAnimatePanelTransforms(true), 500); }); + useEffect(() => { + if (expandedPanelId) { + setAnimatePanelTransforms(false); + } else { + // delaying enabling CSS transforms to the next tick prevents a panel slide animation on minimize + setTimeout(() => setAnimatePanelTransforms(true), 0); + } + }, [expandedPanelId]); + const { onPanelStatusChange } = useDashboardPerformanceTracker({ panelCount: Object.keys(panels).length, }); @@ -98,7 +107,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { 'dshLayout-withoutMargins': !useMargins, 'dshLayout--viewing': viewMode === ViewMode.VIEW, 'dshLayout--editing': viewMode !== ViewMode.VIEW, - 'dshLayout--noAnimation': !animatePanelTransforms, + 'dshLayout--noAnimation': !animatePanelTransforms || expandedPanelId, 'dshLayout-isMaximizedPanel': expandedPanelId !== undefined, }); diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx index 45aa70fd50feb..39ff6ebc48418 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid_item.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'; import { EuiLoadingChart } from '@elastic/eui'; import classNames from 'classnames'; @@ -56,6 +56,8 @@ const Item = React.forwardRef( embeddable: { EmbeddablePanel: PanelComponent }, } = pluginServices.getServices(); const container = useDashboardContainer(); + const scrollToPanelId = container.select((state) => state.componentState.scrollToPanelId); + const highlightPanelId = container.select((state) => state.componentState.highlightPanelId); const expandPanel = expandedPanelId !== undefined && expandedPanelId === id; const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id; @@ -66,11 +68,23 @@ const Item = React.forwardRef( printViewport__vis: container.getInput().viewMode === ViewMode.PRINT, }); + useLayoutEffect(() => { + if (typeof ref !== 'function' && ref?.current) { + if (scrollToPanelId === id) { + container.scrollToPanel(ref.current); + } + if (highlightPanelId === id) { + container.highlightPanel(ref.current); + } + } + }, [id, container, scrollToPanelId, highlightPanelId, ref]); + return (
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss b/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss index f04e5e29d960b..f8715220ddf37 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss @@ -11,6 +11,10 @@ box-shadow: none; border-radius: 0; } + + .dshDashboardGrid__item--highlighted { + border-radius: 0; + } } // Remove border color unless in editing mode @@ -25,3 +29,24 @@ cursor: default; } } + +@keyframes highlightOutline { + 0% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, 1); + } + 25% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, .5); + } + 100% { + outline: solid $euiSizeXS transparentize($euiColorSuccess, 1); + } +} + +.dshDashboardGrid__item--highlighted { + border-radius: $euiSizeXS; + animation-name: highlightOutline; + animation-duration: 4s; + animation-timing-function: ease-out; + // keeps outline from getting cut off by other panels without margins + z-index: 999 !important; +} diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts index 77b51874319ba..e570e1eadd6ca 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts @@ -24,6 +24,7 @@ export interface IPanelPlacementArgs { width: number; height: number; currentPanels: { [key: string]: DashboardPanelState }; + scrollToPanel?: boolean; } export interface IPanelPlacementBesideArgs extends IPanelPlacementArgs { diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts index ef4f4dc7ea5c9..c708937e3d56e 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/add_panel_from_library.ts @@ -41,6 +41,10 @@ export function addFromLibrary(this: DashboardContainer) { notifications, overlays, theme, + onAddPanel: (id: string) => { + this.setScrollToPanelId(id); + this.setHighlightPanelId(id); + }, }) ); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts index ee57970a93cd4..7b02001a93c6c 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/api/panel_management.ts @@ -86,11 +86,7 @@ export async function replacePanel( }; } - await this.updateInput({ - panels, - lastReloadRequestTime: new Date().getTime(), - }); - + await this.updateInput({ panels }); return panelId; } @@ -132,7 +128,12 @@ export function showPlaceholderUntil newStateComplete) - .then((newPanelState: Partial) => - this.replacePanel(placeholderPanelState, newPanelState) - ); + .then(async (newPanelState: Partial) => { + const panelId = await this.replacePanel(placeholderPanelState, newPanelState); + + if (placementArgs?.scrollToPanel) { + this.setScrollToPanelId(panelId); + this.setHighlightPanelId(panelId); + } + }); } diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts index 7609a4f3eb95f..f0a20e832e431 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts @@ -181,12 +181,13 @@ export const createDashboard = async ( const incomingEmbeddable = creationOptions?.incomingEmbeddable; if (incomingEmbeddable) { initialInput.viewMode = ViewMode.EDIT; // view mode must always be edit to recieve an embeddable. - if ( + + const panelExists = incomingEmbeddable.embeddableId && - Boolean(initialInput.panels[incomingEmbeddable.embeddableId]) - ) { + Boolean(initialInput.panels[incomingEmbeddable.embeddableId]); + if (panelExists) { // this embeddable already exists, we will update the explicit input. - const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId]; + const panelToUpdate = initialInput.panels[incomingEmbeddable.embeddableId as string]; const sameType = panelToUpdate.type === incomingEmbeddable.type; panelToUpdate.type = incomingEmbeddable.type; @@ -195,17 +196,22 @@ export const createDashboard = async ( ...(sameType ? panelToUpdate.explicitInput : {}), ...incomingEmbeddable.input, - id: incomingEmbeddable.embeddableId, + id: incomingEmbeddable.embeddableId as string, // maintain hide panel titles setting. hidePanelTitles: panelToUpdate.explicitInput.hidePanelTitles, }; } else { // otherwise this incoming embeddable is brand new and can be added via the default method after the dashboard container is created. - untilDashboardReady().then((container) => - container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input) - ); + untilDashboardReady().then(async (container) => { + container.addNewEmbeddable(incomingEmbeddable.type, incomingEmbeddable.input); + }); } + + untilDashboardReady().then(async (container) => { + container.setScrollToPanelId(incomingEmbeddable.embeddableId); + container.setHighlightPanelId(incomingEmbeddable.embeddableId); + }); } // -------------------------------------------------------------------------------------- diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx index 5b7a589afa950..d5a5385e779b3 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -398,4 +398,41 @@ export class DashboardContainer extends Container { + this.dispatch.setScrollToPanelId(id); + }; + + public scrollToPanel = async (panelRef: HTMLDivElement) => { + const id = this.getState().componentState.scrollToPanelId; + if (!id) return; + + this.untilEmbeddableLoaded(id).then(() => { + this.setScrollToPanelId(undefined); + panelRef.scrollIntoView({ block: 'center' }); + }); + }; + + public scrollToTop = () => { + window.scroll(0, 0); + }; + + public setHighlightPanelId = (id: string | undefined) => { + this.dispatch.setHighlightPanelId(id); + }; + + public highlightPanel = (panelRef: HTMLDivElement) => { + const id = this.getState().componentState.highlightPanelId; + + if (id && panelRef) { + this.untilEmbeddableLoaded(id).then(() => { + panelRef.classList.add('dshDashboardGrid__item--highlighted'); + // Removes the class after the highlight animation finishes + setTimeout(() => { + panelRef.classList.remove('dshDashboardGrid__item--highlighted'); + }, 5000); + }); + } + this.setHighlightPanelId(undefined); + }; } diff --git a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts index 70bf3a7d65989..86a58bb72f639 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/dashboard_container_reducers.ts @@ -209,4 +209,12 @@ export const dashboardContainerReducers = { setHasOverlays: (state: DashboardReduxState, action: PayloadAction) => { state.componentState.hasOverlays = action.payload; }, + + setScrollToPanelId: (state: DashboardReduxState, action: PayloadAction) => { + state.componentState.scrollToPanelId = action.payload; + }, + + setHighlightPanelId: (state: DashboardReduxState, action: PayloadAction) => { + state.componentState.highlightPanelId = action.payload; + }, }; diff --git a/src/plugins/dashboard/public/dashboard_container/types.ts b/src/plugins/dashboard/public/dashboard_container/types.ts index 6e8ff1f5c98a0..544317d9f6bcc 100644 --- a/src/plugins/dashboard/public/dashboard_container/types.ts +++ b/src/plugins/dashboard/public/dashboard_container/types.ts @@ -33,6 +33,8 @@ export interface DashboardPublicState { fullScreenMode?: boolean; savedQueryId?: string; lastSavedId?: string; + scrollToPanelId?: string; + highlightPanelId?: string; } export interface DashboardRenderPerformanceStats { 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 e356c76ddb989..e76cf6960cc23 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 @@ -149,7 +149,7 @@ export const WithFieldEditorDependencies = }; const mergedDependencies = merge({}, dependencies, overridingDependencies); - const previewController = new PreviewController({ dataView, search }); + const previewController = new PreviewController({ dataView, search, fieldFormats }); return ( 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 fddf14c864743..077471b44c237 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 @@ -24,7 +24,7 @@ import { } from '../../../shared_imports'; import type { RuntimeFieldPainlessError } from '../../../types'; import { painlessErrorToMonacoMarker } from '../../../lib'; -import { useFieldPreviewContext, Context } from '../../preview'; +import { useFieldPreviewContext } from '../../preview'; import { schema } from '../form_schema'; import type { FieldFormInternal } from '../field_editor'; import { useStateSelector } from '../../../state_utils'; @@ -57,6 +57,7 @@ const mapReturnTypeToPainlessContext = (runtimeType: RuntimeType): PainlessConte const currentDocumentSelector = (state: PreviewState) => state.documents[state.currentIdx]; const currentDocumentIsLoadingSelector = (state: PreviewState) => state.isLoadingDocuments; +const currentErrorSelector = (state: PreviewState) => state.previewResponse?.error; const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Props) => { const { @@ -66,14 +67,15 @@ const ScriptFieldComponent = ({ existingConcreteFields, links, placeholder }: Pr const editorValidationSubscription = useRef(); const fieldCurrentValue = useRef(''); - const { error, isLoadingPreview, isPreviewAvailable, controller } = useFieldPreviewContext(); + const { isLoadingPreview, isPreviewAvailable, controller } = useFieldPreviewContext(); + const error = useStateSelector(controller.state$, currentErrorSelector); const currentDocument = useStateSelector(controller.state$, currentDocumentSelector); const isFetchingDoc = useStateSelector(controller.state$, currentDocumentIsLoadingSelector); const [validationData$, nextValidationData$] = useBehaviorSubject< | { isFetchingDoc: boolean; isLoadingPreview: boolean; - error: Context['error']; + error: PreviewState['previewResponse']['error']; } | undefined >(undefined); 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 391f54581f258..45ecac570dd99 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 @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { EuiComboBoxOptionOption } from '@elastic/eui'; import { fieldValidators, FieldConfig, RuntimeType, ValidationFunc } from '../../shared_imports'; -import type { Context } from '../preview'; import { RUNTIME_FIELD_OPTIONS } from './constants'; +import type { PreviewState } from '../preview/types'; const { containsCharsField, emptyField, numberGreaterThanField } = fieldValidators; const i18nTexts = { @@ -25,7 +25,7 @@ const i18nTexts = { // Validate the painless **script** const painlessScriptValidator: ValidationFunc = async ({ customData: { provider } }) => { - const previewError = (await provider()) as Context['error']; + const previewError = (await provider()) as PreviewState['previewResponse']['error']; if (previewError && previewError.code === 'PAINLESS_SCRIPT_ERROR') { return { 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 60903fae03ea1..c763d08ae470b 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 @@ -85,7 +85,7 @@ export const FieldEditorFlyoutContentContainer = ({ fieldFormats, uiSettings, }: Props) => { - const [controller] = useState(() => new PreviewController({ dataView, search })); + const [controller] = useState(() => new PreviewController({ dataView, search, fieldFormats })); const [isSaving, setIsSaving] = useState(false); const { fields } = dataView; 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 672f0a747991d..005a2c634cf84 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 @@ -16,6 +16,7 @@ import { DocumentsNavPreview } from './documents_nav_preview'; import { FieldPreviewError } from './field_preview_error'; import { PreviewListItem } from './field_list/field_list_item'; import { PreviewFieldList } from './field_list/field_list'; +import { useStateSelector } from '../../state_utils'; import './field_preview.scss'; @@ -28,12 +29,12 @@ export const FieldPreview = () => { value: { name, script, format }, }, isLoadingPreview, - fields, - error, documents: { fetchDocError }, reset, isPreviewAvailable, + controller, } = useFieldPreviewContext(); + const { fields, error } = useStateSelector(controller.state$, (state) => state.previewResponse); // 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 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 f554025ce9f4b..c4cdfad7d4fec 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 @@ -20,7 +20,6 @@ import { renderToString } from 'react-dom/server'; 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 { useStateSelector } from '../../state_utils'; @@ -32,7 +31,6 @@ import type { Context, Params, EsDocument, - ScriptErrorCodes, FetchDocError, FieldPreview, PreviewState, @@ -101,17 +99,11 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro notifications, 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 */ @@ -169,45 +161,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro ); }, [type, script, currentDocId]); - const setPreviewError = useCallback((error: Context['error']) => { - setPreviewResponse((prev) => ({ - ...prev, - error, - })); - }, []); - - const clearPreviewError = useCallback((errorCode: ScriptErrorCodes) => { - setPreviewResponse((prev) => { - const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error; - return { - ...prev, - error, - }; - }); - }, []); - - const valueFormatter = useCallback( - (value: unknown) => { - if (format?.id) { - const formatter = fieldFormats.getInstance(format.id, format.params); - if (formatter) { - return formatter.getConverterFor('html')(value) ?? JSON.stringify(value); - } - } - - if (type) { - const fieldType = castEsToKbnFieldTypeName(type); - const defaultFormatterForType = fieldFormats.getDefaultInstance(fieldType); - if (defaultFormatterForType) { - return defaultFormatterForType.getConverterFor('html')(value) ?? JSON.stringify(value); - } - } - - return defaultValueFormatter(value); - }, - [format, type, fieldFormats] - ); - const fetchSampleDocuments = useCallback( async (limit: number = 50) => { if (typeof limit !== 'number') { @@ -217,7 +170,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro lastExecutePainlessRequestParams.current.documentId = undefined; setIsFetchingDocument(true); - setPreviewResponse({ fields: [], error: null }); + controller.setPreviewResponse({ fields: [], error: null }); const [response, searchError] = await search .search({ @@ -335,14 +288,14 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro const updateSingleFieldPreview = useCallback( (fieldName: string, values: unknown[]) => { const [value] = values; - const formattedValue = valueFormatter(value); + const formattedValue = controller.valueFormatter({ value, type, format }); - setPreviewResponse({ + controller.setPreviewResponse({ fields: [{ key: fieldName, value, formattedValue }], error: null, }); }, - [valueFormatter] + [controller, type, format] ); const updateCompositeFieldPreview = useCallback( @@ -359,7 +312,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro updatedFieldsInScript.push(fieldName); const [value] = values; - const formattedValue = valueFormatter(value); + const formattedValue = controller.valueFormatter({ value, type, format }); return { key: parentName @@ -375,12 +328,12 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro .sort((a, b) => a.key.localeCompare(b.key)); fieldPreview$.current.next(fields); - setPreviewResponse({ + controller.setPreviewResponse({ fields, error: null, }); }, - [valueFormatter, parentName, name, fieldPreview$, fieldName$] + [parentName, name, fieldPreview$, fieldName$, controller, type, format] ); const updatePreview = useCallback(async () => { @@ -437,7 +390,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro const { values, error } = response.data; if (error) { - setPreviewResponse({ + controller.setPreviewResponse({ fields: [ { key: name ?? '', @@ -474,6 +427,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro updateSingleFieldPreview, updateCompositeFieldPreview, currentDocIndex, + controller, ]); const reset = useCallback(() => { @@ -482,7 +436,7 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro previewCount.current = 0; controller.setDocuments([]); - setPreviewResponse({ fields: [], error: null }); + controller.setPreviewResponse({ fields: [], error: null }); setIsLoadingPreview(false); setIsFetchingDocument(false); }, [controller]); @@ -490,8 +444,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro const ctx = useMemo( () => ({ controller, - fields: previewResponse.fields, - error: previewResponse.error, fieldPreview$: fieldPreview$.current, isPreviewAvailable, isLoadingPreview, @@ -521,7 +473,6 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro [ controller, currentIdx, - previewResponse, fieldPreview$, fetchDocError, params, @@ -583,74 +534,70 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro * Whenever the name or the format changes we immediately update the preview */ useEffect(() => { - setPreviewResponse((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}`; - } + const { previewResponse: prev } = controller.state$.getValue(); + const { fields } = prev; - return { - ...field, - key, - }; - }); + let updatedFields: PreviewState['previewResponse']['fields'] = fields.map((field) => { + let key = name ?? ''; - // 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 }, - ]; + if (type === 'composite') { + // restore initial key segement (the parent name), which was not returned + const { 1: fieldName } = field.key.split('.'); + key = `${name ?? ''}.${fieldName}`; } return { - ...prev, - fields: updatedFields, + ...field, + key, }; }); - }, [name, type, parentName]); + + // 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 }]; + } + + controller.setPreviewResponse({ + ...prev, + fields: updatedFields, + }); + }, [name, type, parentName, controller]); /** * Whenever the format changes we immediately update the preview */ useEffect(() => { - setPreviewResponse((prev) => { - const { fields } = prev; + const { previewResponse: prev } = controller.state$.getValue(); + const { fields } = prev; - return { - ...prev, - fields: fields.map((field) => { - const nextValue = - script === null && Boolean(document) - ? get(document?._source, name ?? '') ?? get(document?.fields, name ?? '') // When there is no script we try to read the value from _source/fields - : field?.value; + controller.setPreviewResponse({ + ...prev, + fields: fields.map((field) => { + const nextValue = + script === null && Boolean(document) + ? get(document?._source, name ?? '') ?? get(document?.fields, name ?? '') // When there is no script we try to read the value from _source/fields + : field?.value; - const formattedValue = valueFormatter(nextValue); + const formattedValue = controller.valueFormatter({ value: nextValue, type, format }); - return { - ...field, - value: nextValue, - formattedValue, - }; - }), - }; + return { + ...field, + value: nextValue, + formattedValue, + }; + }), }); - }, [name, script, document, valueFormatter]); + }, [name, script, document, controller, type, format]); useEffect(() => { if (script?.source === undefined) { // Whenever the source is not defined ("Set value" is toggled off or the // script is empty) we clear the error and update the params cache. lastExecutePainlessRequestParams.current.script = undefined; - setPreviewError(null); + controller.setPreviewError(null); } - }, [script?.source, setPreviewError]); + }, [script?.source, controller]); // Handle the validation state coming from the Painless DiagnosticAdapter // (see @kbn-monaco/src/painless/diagnostics_adapter.ts) @@ -677,16 +624,16 @@ export const FieldPreviewProvider: FunctionComponent<{ controller: PreviewContro }), }, }; - setPreviewError(error); + controller.setPreviewError(error); // Make sure to update the lastExecutePainlessRequestParams cache so when the user updates // the script and fixes the syntax the "updatePreview()" will run lastExecutePainlessRequestParams.current.script = script?.source; } else { // Clear possible previous syntax error - clearPreviewError('PAINLESS_SYNTAX_ERROR'); + controller.clearPreviewError('PAINLESS_SYNTAX_ERROR'); } - }, [scriptEditorValidation, script?.source, setPreviewError, clearPreviewError]); + }, [scriptEditorValidation, script?.source, controller]); /** * Whenever updatePreview() changes (meaning whenever a param changes) diff --git a/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts b/src/plugins/data_view_field_editor/public/components/preview/preview_controller.tsx similarity index 60% rename from src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts rename to src/plugins/data_view_field_editor/public/components/preview/preview_controller.tsx index b572827eac06d..8e9f6156c7d7b 100644 --- a/src/plugins/data_view_field_editor/public/components/preview/preview_controller.ts +++ b/src/plugins/data_view_field_editor/public/components/preview/preview_controller.tsx @@ -9,13 +9,23 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { ISearchStart } from '@kbn/data-plugin/public'; import { BehaviorSubject } from 'rxjs'; +import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { renderToString } from 'react-dom/server'; +import React from 'react'; import { PreviewState } from './types'; import { BehaviorObservable } from '../../state_utils'; -import { EsDocument } from './types'; +import { EsDocument, ScriptErrorCodes, Params } from './types'; +import type { FieldFormatsStart } from '../../shared_imports'; + +export const defaultValueFormatter = (value: unknown) => { + const content = typeof value === 'object' ? JSON.stringify(value) : String(value) ?? '-'; + return renderToString(<>{content}); +}; interface PreviewControllerDependencies { dataView: DataView; search: ISearchStart; + fieldFormats: FieldFormatsStart; } const previewStateDefault: PreviewState = { @@ -30,12 +40,14 @@ const previewStateDefault: PreviewState = { documentSource: 'cluster', /** Keep track if the script painless syntax is being validated and if it is valid */ scriptEditorValidation: { isValidating: false, isValid: true, message: null }, + previewResponse: { fields: [], error: null }, }; export class PreviewController { - constructor({ dataView, search }: PreviewControllerDependencies) { + constructor({ dataView, search, fieldFormats }: PreviewControllerDependencies) { this.dataView = dataView; this.search = search; + this.fieldFormats = fieldFormats; this.internalState$ = new BehaviorSubject({ ...previewStateDefault, @@ -44,10 +56,13 @@ export class PreviewController { this.state$ = this.internalState$ as BehaviorObservable; } + // dependencies // @ts-ignore private dataView: DataView; // @ts-ignore private search: ISearchStart; + private fieldFormats: FieldFormatsStart; + private internalState$: BehaviorSubject; state$: BehaviorObservable; @@ -104,4 +119,52 @@ export class PreviewController { setCustomId = (customId?: string) => { this.updateState({ customId }); }; + + setPreviewError = (error: PreviewState['previewResponse']['error']) => { + this.updateState({ + previewResponse: { ...this.internalState$.getValue().previewResponse, error }, + }); + }; + + setPreviewResponse = (previewResponse: PreviewState['previewResponse']) => { + this.updateState({ previewResponse }); + }; + + clearPreviewError = (errorCode: ScriptErrorCodes) => { + const { previewResponse: prev } = this.internalState$.getValue(); + const error = prev.error === null || prev.error?.code === errorCode ? null : prev.error; + this.updateState({ + previewResponse: { + ...prev, + error, + }, + }); + }; + + valueFormatter = ({ + value, + format, + type, + }: { + value: unknown; + format: Params['format']; + type: Params['type']; + }) => { + if (format?.id) { + const formatter = this.fieldFormats.getInstance(format.id, format.params); + if (formatter) { + return formatter.getConverterFor('html')(value) ?? JSON.stringify(value); + } + } + + if (type) { + const fieldType = castEsToKbnFieldTypeName(type); + const defaultFormatterForType = this.fieldFormats.getDefaultInstance(fieldType); + if (defaultFormatterForType) { + return defaultFormatterForType.getConverterFor('html')(value) ?? JSON.stringify(value); + } + } + + return defaultValueFormatter(value); + }; } 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 347e0a709cf28..b4280f54786ac 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 @@ -55,6 +55,11 @@ export interface PreviewState { isValid: boolean; message: string | null; }; + /** Response from the Painless _execute API */ + previewResponse: { + fields: FieldPreview[]; + error: PreviewError | null; + }; } export interface FetchDocError { @@ -108,9 +113,7 @@ export type ChangeSet = Record; export interface Context { controller: PreviewController; - fields: FieldPreview[]; fieldPreview$: BehaviorSubject; - error: PreviewError | null; fieldTypeInfo?: FieldTypeInfo[]; initialPreviewComplete: boolean; params: { diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx index 926b6782b5633..d2fb4e701e4b4 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.test.tsx @@ -7,7 +7,7 @@ */ import { EditPanelAction } from './edit_panel_action'; -import { Embeddable, EmbeddableInput, SavedObjectEmbeddableInput } from '../embeddables'; +import { Embeddable, EmbeddableInput } from '../embeddables'; import { ViewMode } from '../types'; import { ContactCardEmbeddable } from '../test_samples'; import { embeddablePluginMock } from '../../mocks'; @@ -42,7 +42,7 @@ test('is compatible when edit url is available, in edit mode and editable', asyn ).toBe(true); }); -test('redirects to app using state transfer with by value mode', async () => { +test('redirects to app using state transfer', async () => { applicationMock.currentAppId$ = of('superCoolCurrentApp'); const testPath = '/test-path'; const action = new EditPanelAction( @@ -78,32 +78,6 @@ test('redirects to app using state transfer with by value mode', async () => { }); }); -test('redirects to app using state transfer without by value mode', async () => { - applicationMock.currentAppId$ = of('superCoolCurrentApp'); - const testPath = '/test-path'; - const action = new EditPanelAction( - getFactory, - applicationMock, - stateTransferMock, - () => testPath - ); - const embeddable = new EditableEmbeddable( - { id: '123', viewMode: ViewMode.EDIT, savedObjectId: '1234' } as SavedObjectEmbeddableInput, - true - ); - embeddable.getOutput = jest.fn(() => ({ editApp: 'ultraVisualize', editPath: '/123' })); - await action.execute({ embeddable }); - expect(stateTransferMock.navigateToEditor).toHaveBeenCalledWith('ultraVisualize', { - path: '/123', - state: { - originatingApp: 'superCoolCurrentApp', - embeddableId: '123', - valueInput: undefined, - originatingPath: testPath, - }, - }); -}); - test('getHref returns the edit urls', async () => { const action = new EditPanelAction(getFactory, applicationMock, stateTransferMock); expect(action.getHref).toBeDefined(); diff --git a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts index 1dcecf4ac894d..ba59d92cbef60 100644 --- a/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts +++ b/src/plugins/embeddable/public/lib/actions/edit_panel_action.ts @@ -17,7 +17,6 @@ import { IEmbeddable, EmbeddableEditorState, EmbeddableStateTransfer, - SavedObjectEmbeddableInput, EmbeddableInput, Container, } from '../..'; @@ -124,13 +123,11 @@ export class EditPanelAction implements Action { if (app && path) { if (this.currentAppId) { - const byValueMode = !(embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId; - const originatingPath = this.getOriginatingPath?.(); const state: EmbeddableEditorState = { originatingApp: this.currentAppId, - valueInput: byValueMode ? this.getExplicitInput({ embeddable }) : undefined, + valueInput: this.getExplicitInput({ embeddable }), embeddableId: embeddable.id, searchSessionId: embeddable.getInput().searchSessionId, originatingPath, diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx index dcaa3880678ab..ea7c150bf38b8 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/add_panel_flyout.tsx @@ -30,6 +30,7 @@ interface Props { SavedObjectFinder: React.ComponentType; showCreateNewMenu?: boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; + onAddPanel?: (id: string) => void; } interface State { @@ -101,7 +102,7 @@ export class AddPanelFlyout extends React.Component { throw new EmbeddableFactoryNotFoundError(savedObjectType); } - this.props.container.addNewEmbeddable( + const embeddable = await this.props.container.addNewEmbeddable( factoryForSavedObjectType.type, { savedObjectId } ); @@ -109,6 +110,9 @@ export class AddPanelFlyout extends React.Component { this.doTelemetryForAddEvent(this.props.container.type, factoryForSavedObjectType, so); this.showToast(name); + if (this.props.onAddPanel) { + this.props.onAddPanel(embeddable.id); + } }; private doTelemetryForAddEvent( diff --git a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx index 4cc5a7ccb6e11..eb2722dcf9869 100644 --- a/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/lib/panel/panel_header/panel_actions/add_panel/open_add_panel_flyout.tsx @@ -24,6 +24,7 @@ export function openAddPanelFlyout(options: { showCreateNewMenu?: boolean; reportUiCounter?: UsageCollectionStart['reportUiCounter']; theme: ThemeServiceStart; + onAddPanel?: (id: string) => void; }): OverlayRef { const { embeddable, @@ -35,11 +36,13 @@ export function openAddPanelFlyout(options: { showCreateNewMenu, reportUiCounter, theme, + onAddPanel, } = options; const flyoutSession = overlays.openFlyout( toMountPoint( { if (flyoutSession) { flyoutSession.close(); diff --git a/src/plugins/files/public/plugin.ts b/src/plugins/files/public/plugin.ts index 54646e9199f9a..13828d0ee366c 100644 --- a/src/plugins/files/public/plugin.ts +++ b/src/plugins/files/public/plugin.ts @@ -35,7 +35,10 @@ export interface FilesSetup { registerFileKind(fileKind: FileKindBrowser): void; } -export type FilesStart = Pick; +export type FilesStart = Pick & { + getFileKindDefinition: (id: string) => FileKindBrowser; + getAllFindKindDefinitions: () => FileKindBrowser[]; +}; /** * Bringing files to Kibana @@ -77,6 +80,12 @@ export class FilesPlugin implements Plugin { start(core: CoreStart): FilesStart { return { filesClientFactory: this.filesClientFactory!, + getFileKindDefinition: (id: string): FileKindBrowser => { + return this.registry.get(id); + }, + getAllFindKindDefinitions: (): FileKindBrowser[] => { + return this.registry.getAll(); + }, }; } } diff --git a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts index 0f453a1b81e6a..014e57b41d2b1 100644 --- a/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts +++ b/src/plugins/files/server/file_client/file_metadata_client/adapters/query_filters.ts @@ -24,6 +24,7 @@ export function filterArgsToKuery({ extension, mimeType, kind, + kindToExclude, meta, name, status, @@ -50,12 +51,27 @@ export function filterArgsToKuery({ } }; + const addExcludeFilters = (fieldName: keyof FileMetadata | string, values: string[] = []) => { + if (values.length) { + const andExpressions = values + .filter(Boolean) + .map((value) => + nodeTypes.function.buildNode( + 'not', + nodeBuilder.is(`${attrPrefix}.${fieldName}`, escapeKuery(value)) + ) + ); + kueryExpressions.push(nodeBuilder.and(andExpressions)); + } + }; + addFilters('name', name, true); addFilters('FileKind', kind); addFilters('Status', status); addFilters('extension', extension); addFilters('mime_type', mimeType); addFilters('user.id', user); + addExcludeFilters('FileKind', kindToExclude); if (meta) { const addMetaFilters = pipe( diff --git a/src/plugins/files/server/file_service/file_action_types.ts b/src/plugins/files/server/file_service/file_action_types.ts index 4247f567802ed..96795ac93b387 100644 --- a/src/plugins/files/server/file_service/file_action_types.ts +++ b/src/plugins/files/server/file_service/file_action_types.ts @@ -82,6 +82,10 @@ export interface FindFileArgs extends Pagination { * File kind(s), see {@link FileKind}. */ kind?: string[]; + /** + * File kind(s) to exclude from search, see {@link FileKind}. + */ + kindToExclude?: string[]; /** * File name(s). */ diff --git a/src/plugins/files/server/integration_tests/file_service.test.ts b/src/plugins/files/server/integration_tests/file_service.test.ts index 25d7f463de03a..3492eb8e5f12c 100644 --- a/src/plugins/files/server/integration_tests/file_service.test.ts +++ b/src/plugins/files/server/integration_tests/file_service.test.ts @@ -157,26 +157,39 @@ describe('FileService', () => { createDisposableFile({ fileKind, name: 'foo-2' }), createDisposableFile({ fileKind, name: 'foo-3' }), createDisposableFile({ fileKind, name: 'test-3' }), + createDisposableFile({ fileKind: fileKindNonDefault, name: 'foo-1' }), ]); { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 1, }); expect(files.length).toBe(2); - expect(total).toBe(3); + expect(total).toBe(4); } { const { files, total } = await fileService.find({ - kind: [fileKind], + kind: [fileKind, fileKindNonDefault], name: ['foo*'], perPage: 2, page: 2, }); - expect(files.length).toBe(1); + expect(files.length).toBe(2); + expect(total).toBe(4); + } + + // Filter out fileKind + { + const { files, total } = await fileService.find({ + kindToExclude: [fileKindNonDefault], + name: ['foo*'], + perPage: 10, + page: 1, + }); + expect(files.length).toBe(3); // foo-1 from fileKindNonDefault not returned expect(total).toBe(3); } }); diff --git a/src/plugins/files/server/routes/find.ts b/src/plugins/files/server/routes/find.ts index a81a9d2ea5220..6749e06254100 100644 --- a/src/plugins/files/server/routes/find.ts +++ b/src/plugins/files/server/routes/find.ts @@ -30,6 +30,7 @@ export function toArrayOrUndefined(val?: string | string[]): undefined | string[ const rt = { body: schema.object({ kind: schema.maybe(stringOrArrayOfStrings), + kindToExclude: schema.maybe(stringOrArrayOfStrings), status: schema.maybe(stringOrArrayOfStrings), extension: schema.maybe(stringOrArrayOfStrings), name: schema.maybe(nameStringOrArrayOfNameStrings), @@ -50,12 +51,13 @@ export type Endpoint = CreateRouteDefinition< const handler: CreateHandler = async ({ files }, req, res) => { const { fileService } = await files; const { - body: { meta, extension, kind, name, status }, + body: { meta, extension, kind, name, status, kindToExclude }, query, } = req; const { files: results, total } = await fileService.asCurrentUser().find({ kind: toArrayOrUndefined(kind), + kindToExclude: toArrayOrUndefined(kindToExclude), name: toArrayOrUndefined(name), status: toArrayOrUndefined(status), extension: toArrayOrUndefined(extension), diff --git a/src/plugins/files_management/public/app.tsx b/src/plugins/files_management/public/app.tsx index becdd05fa0e2c..3ee4e5f52720c 100644 --- a/src/plugins/files_management/public/app.tsx +++ b/src/plugins/files_management/public/app.tsx @@ -12,20 +12,33 @@ import { EuiButtonEmpty } from '@elastic/eui'; import { TableListView, UserContentCommonSchema } from '@kbn/content-management-table-list'; import numeral from '@elastic/numeral'; import type { FileJSON } from '@kbn/files-plugin/common'; + import { useFilesManagementContext } from './context'; import { i18nTexts } from './i18n_texts'; import { EmptyPrompt, DiagnosticsFlyout, FileFlyout } from './components'; -type FilesUserContentSchema = UserContentCommonSchema; +type FilesUserContentSchema = Omit & { + attributes: { + title: string; + description?: string; + fileKind: string; + }; +}; function naivelyFuzzify(query: string): string { return query.includes('*') ? query : `*${query}*`; } export const App: FunctionComponent = () => { - const { filesClient } = useFilesManagementContext(); + const { filesClient, getFileKindDefinition, getAllFindKindDefinitions } = + useFilesManagementContext(); const [showDiagnosticsFlyout, setShowDiagnosticsFlyout] = useState(false); const [selectedFile, setSelectedFile] = useState(undefined); + + const kindToExcludeFromSearch = getAllFindKindDefinitions() + .filter(({ managementUiActions }) => managementUiActions?.list?.enabled === false) + .map(({ id }) => id); + return (
@@ -37,7 +50,10 @@ export const App: FunctionComponent = () => { entityNamePlural={i18nTexts.entityNamePlural} findItems={(searchQuery) => filesClient - .find({ name: searchQuery ? naivelyFuzzify(searchQuery) : undefined }) + .find({ + name: searchQuery ? naivelyFuzzify(searchQuery) : undefined, + kindToExclude: kindToExcludeFromSearch, + }) .then(({ files, total }) => ({ hits: files.map((file) => ({ id: file.id, @@ -71,6 +87,12 @@ export const App: FunctionComponent = () => { {i18nTexts.diagnosticsFlyoutTitle} , ]} + rowItemActions={({ attributes }) => { + const definition = getFileKindDefinition(attributes.fileKind); + return { + delete: definition?.managementUiActions?.delete, + }; + }} /> {showDiagnosticsFlyout && ( setShowDiagnosticsFlyout(false)} /> diff --git a/src/plugins/files_management/public/context.tsx b/src/plugins/files_management/public/context.tsx index 18f031b84e5c1..0688c5a7edecb 100644 --- a/src/plugins/files_management/public/context.tsx +++ b/src/plugins/files_management/public/context.tsx @@ -12,9 +12,16 @@ import type { AppContext } from './types'; const FilesManagementAppContext = createContext(null as unknown as AppContext); -export const FilesManagementAppContextProvider: FC = ({ children, filesClient }) => { +export const FilesManagementAppContextProvider: FC = ({ + children, + filesClient, + getFileKindDefinition, + getAllFindKindDefinitions, +}) => { return ( - + {children} ); diff --git a/src/plugins/files_management/public/i18n_texts.ts b/src/plugins/files_management/public/i18n_texts.ts index c5f4956af372f..d430038dcdddc 100644 --- a/src/plugins/files_management/public/i18n_texts.ts +++ b/src/plugins/files_management/public/i18n_texts.ts @@ -101,4 +101,7 @@ export const i18nTexts = { defaultMessage: 'Upload error', }), } as Record, + rowCheckboxDisabled: i18n.translate('filesManagement.table.checkBoxDisabledLabel', { + defaultMessage: 'This file cannot be deleted.', + }), }; diff --git a/src/plugins/files_management/public/mount_management_section.tsx b/src/plugins/files_management/public/mount_management_section.tsx index 7dce1986237a7..9c7091516d46e 100755 --- a/src/plugins/files_management/public/mount_management_section.tsx +++ b/src/plugins/files_management/public/mount_management_section.tsx @@ -30,6 +30,10 @@ export const mountManagementSection = ( startDeps: StartDependencies, { element, history }: ManagementAppMountParams ) => { + const { + files: { filesClientFactory, getAllFindKindDefinitions, getFileKindDefinition }, + } = startDeps; + ReactDOM.render( @@ -41,7 +45,9 @@ export const mountManagementSection = ( }} > diff --git a/src/plugins/files_management/public/types.ts b/src/plugins/files_management/public/types.ts index 2a73b69bea017..303d5e1c5d1a7 100755 --- a/src/plugins/files_management/public/types.ts +++ b/src/plugins/files_management/public/types.ts @@ -11,6 +11,8 @@ import { ManagementSetup } from '@kbn/management-plugin/public'; export interface AppContext { filesClient: FilesClient; + getFileKindDefinition: FilesStart['getFileKindDefinition']; + getAllFindKindDefinitions: FilesStart['getAllFindKindDefinitions']; } export interface SetupDependencies { diff --git a/src/plugins/newsfeed/public/components/flyout_list.tsx b/src/plugins/newsfeed/public/components/flyout_list.tsx index 23be44230e220..c09524d384e86 100644 --- a/src/plugins/newsfeed/public/components/flyout_list.tsx +++ b/src/plugins/newsfeed/public/components/flyout_list.tsx @@ -32,11 +32,11 @@ import { NewsLoadingPrompt } from './loading_news'; export const NewsfeedFlyout = (props: Partial & { showPlainSpinner: boolean }) => { const { newsFetchResult, setFlyoutVisible } = useContext(NewsfeedContext); const closeFlyout = useCallback(() => setFlyoutVisible(false), [setFlyoutVisible]); - + const { showPlainSpinner, ...rest } = props; return ( { expect( fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'string' } as any) ).toBe(true); + + expect( + fieldSupportsBreakdown({ aggregatable: true, scripted: false, type: 'number' } as any) + ).toBe(true); + }); + + it('should return false if field is aggregatable but it is a time series counter metric', () => { + expect( + fieldSupportsBreakdown({ + aggregatable: true, + scripted: false, + type: 'number', + timeSeriesMetric: 'counter', + } as any) + ).toBe(false); }); }); diff --git a/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts b/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts index 302a5950fefcb..88ec604c1462e 100644 --- a/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts +++ b/src/plugins/unified_histogram/public/chart/utils/field_supports_breakdown.ts @@ -11,4 +11,7 @@ import { DataViewField } from '@kbn/data-views-plugin/public'; const supportedTypes = new Set(['string', 'boolean', 'number', 'ip']); export const fieldSupportsBreakdown = (field: DataViewField) => - supportedTypes.has(field.type) && field.aggregatable && !field.scripted; + supportedTypes.has(field.type) && + field.aggregatable && + !field.scripted && + field.timeSeriesMetric !== 'counter'; diff --git a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx index 8ca32108a2555..aecd588673e09 100644 --- a/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx +++ b/src/plugins/unified_search/public/query_string_input/query_bar_top_row.tsx @@ -120,6 +120,7 @@ export interface QueryBarTopRowProps isScreenshotMode?: boolean; onTextLangQuerySubmit: (query?: Query | AggregateQuery) => void; onTextLangQueryChange: (query: AggregateQuery) => void; + submitOnBlur?: boolean; } export const SharingMetaFields = React.memo(function SharingMetaFields({ @@ -556,6 +557,7 @@ export const QueryBarTopRow = React.memo( size={props.suggestionsSize} isDisabled={props.isDisabled} appName={appName} + submitOnBlur={props.submitOnBlur} deps={{ unifiedSearch, data, diff --git a/src/plugins/unified_search/public/search_bar/search_bar.tsx b/src/plugins/unified_search/public/search_bar/search_bar.tsx index 0a83a45de1d03..3326d81e29109 100644 --- a/src/plugins/unified_search/public/search_bar/search_bar.tsx +++ b/src/plugins/unified_search/public/search_bar/search_bar.tsx @@ -105,6 +105,8 @@ export interface SearchBarOwnProps { * Disables all inputs and interactive elements, */ isDisabled?: boolean; + + submitOnBlur?: boolean; } export type SearchBarProps = SearchBarOwnProps & @@ -585,6 +587,7 @@ class SearchBarUI extends C isScreenshotMode={this.props.isScreenshotMode} onTextLangQuerySubmit={this.onTextLangQuerySubmit} onTextLangQueryChange={this.onTextLangQueryChange} + submitOnBlur={this.props.submitOnBlur} />
); diff --git a/tsconfig.base.json b/tsconfig.base.json index 6bede7391b146..e96e5439e9c2c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -904,6 +904,8 @@ "@kbn/ml-agg-utils/*": ["x-pack/packages/ml/agg_utils/*"], "@kbn/ml-date-picker": ["x-pack/packages/ml/date_picker"], "@kbn/ml-date-picker/*": ["x-pack/packages/ml/date_picker/*"], + "@kbn/ml-error-utils": ["x-pack/packages/ml/error_utils"], + "@kbn/ml-error-utils/*": ["x-pack/packages/ml/error_utils/*"], "@kbn/ml-is-defined": ["x-pack/packages/ml/is_defined"], "@kbn/ml-is-defined/*": ["x-pack/packages/ml/is_defined/*"], "@kbn/ml-is-populated-object": ["x-pack/packages/ml/is_populated_object"], @@ -1360,6 +1362,8 @@ "@kbn/url-drilldown-plugin/*": ["x-pack/plugins/drilldowns/url_drilldown/*"], "@kbn/url-forwarding-plugin": ["src/plugins/url_forwarding"], "@kbn/url-forwarding-plugin/*": ["src/plugins/url_forwarding/*"], + "@kbn/url-state": ["packages/kbn-url-state"], + "@kbn/url-state/*": ["packages/kbn-url-state/*"], "@kbn/usage-collection-plugin": ["src/plugins/usage_collection"], "@kbn/usage-collection-plugin/*": ["src/plugins/usage_collection/*"], "@kbn/usage-collection-test-plugin": ["test/plugin_functional/plugins/usage_collection"], diff --git a/x-pack/packages/ml/error_utils/README.md b/x-pack/packages/ml/error_utils/README.md new file mode 100644 index 0000000000000..d8f2837dffbd3 --- /dev/null +++ b/x-pack/packages/ml/error_utils/README.md @@ -0,0 +1,3 @@ +# @kbn/ml-error-utils + +Empty package generated by @kbn/generate diff --git a/x-pack/plugins/ml/common/util/errors/index.ts b/x-pack/packages/ml/error_utils/index.ts similarity index 65% rename from x-pack/plugins/ml/common/util/errors/index.ts rename to x-pack/packages/ml/error_utils/index.ts index f6566c98490da..9eac8a4d1c70b 100644 --- a/x-pack/plugins/ml/common/util/errors/index.ts +++ b/x-pack/packages/ml/error_utils/index.ts @@ -5,8 +5,8 @@ * 2.0. */ -export { MLRequestFailure } from './request_error'; -export { extractErrorMessage, extractErrorProperties } from './process_errors'; +export { MLRequestFailure } from './src/request_error'; +export { extractErrorMessage, extractErrorProperties } from './src/process_errors'; export type { ErrorType, ErrorMessage, @@ -14,6 +14,8 @@ export type { EsErrorRootCause, MLErrorObject, MLHttpFetchError, + MLHttpFetchErrorBase, MLResponseError, -} from './types'; -export { isBoomError, isErrorString, isEsErrorBody, isMLResponseError } from './types'; + QueryErrorMessage, +} from './src/types'; +export { isBoomError, isErrorString, isEsErrorBody, isMLResponseError } from './src/types'; diff --git a/x-pack/packages/ml/error_utils/jest.config.js b/x-pack/packages/ml/error_utils/jest.config.js new file mode 100644 index 0000000000000..f5da401040575 --- /dev/null +++ b/x-pack/packages/ml/error_utils/jest.config.js @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/x-pack/packages/ml/error_utils'], +}; diff --git a/x-pack/packages/ml/error_utils/kibana.jsonc b/x-pack/packages/ml/error_utils/kibana.jsonc new file mode 100644 index 0000000000000..7629766aca7a7 --- /dev/null +++ b/x-pack/packages/ml/error_utils/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/ml-error-utils", + "owner": "@elastic/ml-ui" +} diff --git a/x-pack/packages/ml/error_utils/package.json b/x-pack/packages/ml/error_utils/package.json new file mode 100644 index 0000000000000..9f0e6c09ef578 --- /dev/null +++ b/x-pack/packages/ml/error_utils/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/ml-error-utils", + "private": true, + "version": "1.0.0", + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/plugins/ml/common/util/errors/errors.test.ts b/x-pack/packages/ml/error_utils/src/process_errors.test.ts similarity index 95% rename from x-pack/plugins/ml/common/util/errors/errors.test.ts rename to x-pack/packages/ml/error_utils/src/process_errors.test.ts index 5b1e113d06f92..5624c2f0c7477 100644 --- a/x-pack/plugins/ml/common/util/errors/errors.test.ts +++ b/x-pack/packages/ml/error_utils/src/process_errors.test.ts @@ -7,7 +7,8 @@ import Boom from '@hapi/boom'; -import { extractErrorMessage, MLHttpFetchError, EsErrorBody } from '.'; +import { extractErrorMessage } from './process_errors'; +import { type MLHttpFetchError, type EsErrorBody } from './types'; describe('ML - error message utils', () => { describe('extractErrorMessage', () => { diff --git a/x-pack/plugins/ml/common/util/errors/process_errors.ts b/x-pack/packages/ml/error_utils/src/process_errors.ts similarity index 86% rename from x-pack/plugins/ml/common/util/errors/process_errors.ts rename to x-pack/packages/ml/error_utils/src/process_errors.ts index 0da2650fa5fd6..1a50fd6ce3494 100644 --- a/x-pack/plugins/ml/common/util/errors/process_errors.ts +++ b/x-pack/packages/ml/error_utils/src/process_errors.ts @@ -16,15 +16,19 @@ import { isMLResponseError, } from './types'; +/** + * Extract properties of the error object from within the response error + * coming from Kibana, Elasticsearch, and our own ML messages. + * + * @param {ErrorType} error + * @returns {MLErrorObject} + */ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { - // extract properties of the error object from within the response error - // coming from Kibana, Elasticsearch, and our own ML messages - // some responses contain raw es errors as part of a bulk response // e.g. if some jobs fail the action in a bulk request if (isEsErrorBody(error)) { return { - message: error.error.reason, + message: error.error.reason ?? '', statusCode: error.status, fullError: error, }; @@ -79,7 +83,7 @@ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { typeof error.body.attributes.body.error.root_cause[0] === 'object' && isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) ) { - errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; + errObj.causedBy = error.body.attributes.body.error.root_cause[0].script as string; errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; } return errObj; @@ -103,8 +107,14 @@ export const extractErrorProperties = (error: ErrorType): MLErrorObject => { }; }; +/** + * Extract only the error message within the response error + * coming from Kibana, Elasticsearch, and our own ML messages. + * + * @param {ErrorType} error + * @returns {string} + */ export const extractErrorMessage = (error: ErrorType): string => { - // extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages const errorObj = extractErrorProperties(error); return errorObj.message; }; diff --git a/x-pack/plugins/ml/common/util/errors/request_error.ts b/x-pack/packages/ml/error_utils/src/request_error.ts similarity index 75% rename from x-pack/plugins/ml/common/util/errors/request_error.ts rename to x-pack/packages/ml/error_utils/src/request_error.ts index 57d63e6cf54b8..a370129871323 100644 --- a/x-pack/plugins/ml/common/util/errors/request_error.ts +++ b/x-pack/packages/ml/error_utils/src/request_error.ts @@ -7,7 +7,22 @@ import { MLErrorObject, ErrorType } from './types'; +/** + * ML Request Failure + * + * @export + * @class MLRequestFailure + * @typedef {MLRequestFailure} + * @extends {Error} + */ export class MLRequestFailure extends Error { + /** + * Creates an instance of MLRequestFailure. + * + * @constructor + * @param {MLErrorObject} error + * @param {ErrorType} resp + */ constructor(error: MLErrorObject, resp: ErrorType) { super(error.message); Object.setPrototypeOf(this, new.target.prototype); diff --git a/x-pack/packages/ml/error_utils/src/types.ts b/x-pack/packages/ml/error_utils/src/types.ts new file mode 100644 index 0000000000000..b66c960b8c8c0 --- /dev/null +++ b/x-pack/packages/ml/error_utils/src/types.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 Boom from '@hapi/boom'; + +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { IHttpFetchError } from '@kbn/core-http-browser'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +/** + * Short hand type of estypes.ErrorCause. + * @typedef {EsErrorRootCause} + */ +export type EsErrorRootCause = estypes.ErrorCause; + +/** + * Short hand type of estypes.ErrorResponseBase. + * @typedef {EsErrorBody} + */ +export type EsErrorBody = estypes.ErrorResponseBase; + +/** + * ML Response error + * @export + * @interface MLResponseError + * @typedef {MLResponseError} + */ +export interface MLResponseError { + /** + * statusCode + * @type {number} + */ + statusCode: number; + /** + * error + * @type {string} + */ + error: string; + /** + * message + * @type {string} + */ + message: string; + /** + * Optional attributes + * @type {?{ + body: EsErrorBody; + }} + */ + attributes?: { + body: EsErrorBody; + }; +} + +/** + * Interface holding error message + * @export + * @interface ErrorMessage + * @typedef {ErrorMessage} + */ +export interface ErrorMessage { + /** + * message + * @type {string} + */ + message: string; +} + +/** + * To be used for client side errors related to search query bars. + */ +export interface QueryErrorMessage extends ErrorMessage { + /** + * query + * @type {string} + */ + query: string; +} + +/** + * ML Error Object + * @export + * @interface MLErrorObject + * @typedef {MLErrorObject} + */ +export interface MLErrorObject { + /** + * Optional causedBy + * @type {?string} + */ + causedBy?: string; + /** + * message + * @type {string} + */ + message: string; + /** + * Optional statusCode + * @type {?number} + */ + statusCode?: number; + /** + * Optional fullError + * @type {?EsErrorBody} + */ + fullError?: EsErrorBody; +} + +/** + * MLHttpFetchErrorBase + * @export + * @interface MLHttpFetchErrorBase + * @typedef {MLHttpFetchErrorBase} + * @template T + * @extends {IHttpFetchError} + */ +export interface MLHttpFetchErrorBase extends IHttpFetchError { + /** + * body + * @type {T} + */ + body: T; +} + +/** + * MLHttpFetchError + * @export + * @typedef {MLHttpFetchError} + */ +export type MLHttpFetchError = MLHttpFetchErrorBase; + +/** + * Union type of error types + * @export + * @typedef {ErrorType} + */ +export type ErrorType = MLHttpFetchError | EsErrorBody | Boom.Boom | string | undefined; + +/** + * Type guard to check if error is of type EsErrorBody + * @export + * @param {unknown} error + * @returns {error is EsErrorBody} + */ +export function isEsErrorBody(error: unknown): error is EsErrorBody { + return isPopulatedObject(error, ['error']) && isPopulatedObject(error.error, ['reason']); +} + +/** + * Type guard to check if error is a string. + * @export + * @param {unknown} error + * @returns {error is string} + */ +export function isErrorString(error: unknown): error is string { + return typeof error === 'string'; +} + +/** + * Type guard to check if error is of type ErrorMessage. + * @export + * @param {unknown} error + * @returns {error is ErrorMessage} + */ +export function isErrorMessage(error: unknown): error is ErrorMessage { + return isPopulatedObject(error, ['message']) && typeof error.message === 'string'; +} + +/** + * Type guard to check if error is of type MLResponseError. + * @export + * @param {unknown} error + * @returns {error is MLResponseError} + */ +export function isMLResponseError(error: unknown): error is MLResponseError { + return ( + isPopulatedObject(error, ['body']) && + isPopulatedObject(error.body, ['message']) && + 'message' in error.body + ); +} + +/** + * Type guard to check if error is of type Boom. + * @export + * @param {unknown} error + * @returns {error is Boom.Boom} + */ +export function isBoomError(error: unknown): error is Boom.Boom { + return isPopulatedObject(error, ['isBoom']) && error.isBoom === true; +} diff --git a/x-pack/packages/ml/error_utils/tsconfig.json b/x-pack/packages/ml/error_utils/tsconfig.json new file mode 100644 index 0000000000000..de1c550b0e1ab --- /dev/null +++ b/x-pack/packages/ml/error_utils/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/ml-is-populated-object", + "@kbn/core-http-browser", + ] +} diff --git a/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts b/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts new file mode 100644 index 0000000000000..6f67c4dd39ea8 --- /dev/null +++ b/x-pack/plugins/actions/server/lib/mustache_lambdas.test.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import dedent from 'dedent'; + +import { renderMustacheString } from './mustache_renderer'; + +describe('mustache lambdas', () => { + describe('FormatDate', () => { + it('date with defaults is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 03:52pm'); + }); + + it('date with a time zone is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} ; America/New_York {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual('2022-11-29 10:52am'); + }); + + it('date with a format is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}} ;; dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none')).toEqual( + 'Tuesday Nov 29th 2022 15:52:44.000' + ); + }); + + it('date with a format and timezone is successful', () => { + const timeStamp = '2022-11-29T15:52:44Z'; + const template = dedent` + {{#FormatDate}} {{timeStamp}};America/New_York;dddd MMM Do YYYY HH:mm:ss.SSS {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'Tuesday Nov 29th 2022 10:52:44.000' + ); + }); + + it('empty date produces error', () => { + const timeStamp = ''; + const template = dedent` + {{#FormatDate}} {{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}} {{/FormatDate}}": date is empty' + ); + }); + + it('invalid date produces error', () => { + const timeStamp = 'this is not a d4t3'; + const template = dedent` + {{#FormatDate}}{{timeStamp}}{{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}}{{timeStamp}}{{/FormatDate}}": invalid date "this is not a d4t3"' + ); + }); + + it('invalid timezone produces error', () => { + const timeStamp = '2023-04-10T23:52:39'; + const template = dedent` + {{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}} + `.trim(); + + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'error rendering mustache template "{{#FormatDate}}{{timeStamp}};NotATime Zone!{{/FormatDate}}": unknown timeZone value "NotATime Zone!"' + ); + }); + + it('invalid format produces error', () => { + const timeStamp = '2023-04-10T23:52:39'; + const template = dedent` + {{#FormatDate}}{{timeStamp}};;garbage{{/FormatDate}} + `.trim(); + + // not clear how to force an error, it pretty much does something with + // ANY string + expect(renderMustacheString(template, { timeStamp }, 'none').trim()).toEqual( + 'gamrbamg2' // a => am/pm (so am here); e => day of week + ); + }); + }); + + describe('EvalMath', () => { + it('math is successful', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const template = dedent` + {{#EvalMath}} 1 + 0 {{/EvalMath}} + {{#EvalMath}} 1 + context.a.b {{/EvalMath}} + {{#context}} + {{#EvalMath}} 1 + c.d {{/EvalMath}} + {{/context}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(result).toEqual(`1\n2\n3\n`); + }); + + it('invalid expression produces error', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const template = dedent` + {{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(result).toEqual( + `error rendering mustache template "{{#EvalMath}} ) 1 ++++ 0 ( {{/EvalMath}}": error evaluating tinymath expression ") 1 ++++ 0 (": Failed to parse expression. Expected "(", function, literal, or whitespace but ")" found.` + ); + }); + }); + + describe('ParseHJson', () => { + it('valid Hjson is successful', () => { + const vars = { + context: { + a: { b: 1 }, + c: { d: 2 }, + }, + }; + const hjson = ` + { + # specify rate in requests/second (because comments are helpful!) + rate: 1000 + + a: {{context.a}} + a_b: {{context.a.b}} + c: {{context.c}} + c_d: {{context.c.d}} + + # list items can be separated by lines, or commas, and trailing + # commas permitted + list: [ + 1 2 + 3 + 4,5,6, + ] + }`; + const template = dedent` + {{#ParseHjson}} ${hjson} {{/ParseHjson}} + `.trim(); + + const result = renderMustacheString(template, vars, 'none'); + expect(JSON.parse(result)).toMatchInlineSnapshot(` + Object { + "a": Object { + "b": 1, + }, + "a_b": 1, + "c": Object { + "d": 2, + }, + "c_d": 2, + "list": Array [ + "1 2", + 3, + 4, + 5, + 6, + ], + "rate": 1000, + } + `); + }); + + it('renders an error message on parse errors', () => { + const template = dedent` + {{#ParseHjson}} [1,2,3,,] {{/ParseHjson}} + `.trim(); + + const result = renderMustacheString(template, {}, 'none'); + expect(result).toMatch(/^error rendering mustache template .*/); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/lib/mustache_lambdas.ts b/x-pack/plugins/actions/server/lib/mustache_lambdas.ts new file mode 100644 index 0000000000000..62ba5621e0e1e --- /dev/null +++ b/x-pack/plugins/actions/server/lib/mustache_lambdas.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 tinymath from '@kbn/tinymath'; +import { parse as hjsonParse } from 'hjson'; + +import moment from 'moment-timezone'; + +type Variables = Record; + +const DefaultDateTimeZone = 'UTC'; +const DefaultDateFormat = 'YYYY-MM-DD hh:mma'; + +export function getMustacheLambdas(): Variables { + return getLambdas(); +} + +const TimeZoneSet = new Set(moment.tz.names()); + +type RenderFn = (text: string) => string; + +function getLambdas() { + return { + EvalMath: () => + // mustache invokes lamdas with `this` set to the current "view" (variables) + function (this: Variables, text: string, render: RenderFn) { + return evalMath(this, render(text.trim())); + }, + ParseHjson: () => + function (text: string, render: RenderFn) { + return parseHjson(render(text.trim())); + }, + FormatDate: () => + function (text: string, render: RenderFn) { + const dateString = render(text.trim()).trim(); + return formatDate(dateString); + }, + }; +} + +function evalMath(vars: Variables, o: unknown): string { + const expr = `${o}`; + try { + const result = tinymath.evaluate(expr, vars); + return `${result}`; + } catch (err) { + throw new Error(`error evaluating tinymath expression "${expr}": ${err.message}`); + } +} + +function parseHjson(o: unknown): string { + const hjsonObject = `${o}`; + let object: unknown; + + try { + object = hjsonParse(hjsonObject); + } catch (err) { + throw new Error(`error parsing Hjson "${hjsonObject}": ${err.message}`); + } + + return JSON.stringify(object); +} + +function formatDate(dateString: unknown): string { + const { date, timeZone, format } = splitDateString(`${dateString}`); + + if (date === '') { + throw new Error(`date is empty`); + } + + if (isNaN(new Date(date).valueOf())) { + throw new Error(`invalid date "${date}"`); + } + + let mDate: moment.Moment; + try { + mDate = moment(date); + if (!mDate.isValid()) { + throw new Error(`date is invalid`); + } + } catch (err) { + throw new Error(`error evaluating moment date "${date}": ${err.message}`); + } + + if (!TimeZoneSet.has(timeZone)) { + throw new Error(`unknown timeZone value "${timeZone}"`); + } + + try { + mDate.tz(timeZone); + } catch (err) { + throw new Error(`error evaluating moment timeZone "${timeZone}": ${err.message}`); + } + + try { + return mDate.format(format); + } catch (err) { + throw new Error(`error evaluating moment format "${format}": ${err.message}`); + } +} + +function splitDateString(dateString: string) { + const parts = dateString.split(';', 3).map((s) => s.trim()); + const [date = '', timeZone = '', format = ''] = parts; + return { + date, + timeZone: timeZone || DefaultDateTimeZone, + format: format || DefaultDateFormat, + }; +} diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts index 964a793d8f81c..3a02ce0d1a983 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.test.ts @@ -58,6 +58,12 @@ describe('mustache_renderer', () => { expect(renderMustacheString('{{f.g}}', variables, escape)).toBe('3'); expect(renderMustacheString('{{f.h}}', variables, escape)).toBe(''); expect(renderMustacheString('{{i}}', variables, escape)).toBe('42,43,44'); + + if (escape === 'markdown') { + expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('\\[42,43,44\\]'); + } else { + expect(renderMustacheString('{{i.asJSON}}', variables, escape)).toBe('[42,43,44]'); + } }); } @@ -339,6 +345,11 @@ describe('mustache_renderer', () => { const expected = '1 - {"c":2,"d":[3,4]} -- 5,{"f":6,"g":7}'; expect(renderMustacheString('{{a}} - {{b}} -- {{e}}', deepVariables, 'none')).toEqual(expected); + + expect(renderMustacheString('{{e}}', deepVariables, 'none')).toEqual('5,{"f":6,"g":7}'); + expect(renderMustacheString('{{e.asJSON}}', deepVariables, 'none')).toEqual( + '[5,{"f":6,"g":7}]' + ); }); describe('converting dot variables', () => { diff --git a/x-pack/plugins/actions/server/lib/mustache_renderer.ts b/x-pack/plugins/actions/server/lib/mustache_renderer.ts index fc4381fa0c9c3..37713167e9a34 100644 --- a/x-pack/plugins/actions/server/lib/mustache_renderer.ts +++ b/x-pack/plugins/actions/server/lib/mustache_renderer.ts @@ -7,8 +7,10 @@ import Mustache from 'mustache'; import { isString, isPlainObject, cloneDeepWith, merge } from 'lodash'; +import { getMustacheLambdas } from './mustache_lambdas'; export type Escape = 'markdown' | 'slack' | 'json' | 'none'; + type Variables = Record; // return a rendered mustache template with no escape given the specified variables and escape @@ -25,11 +27,13 @@ export function renderMustacheStringNoEscape(string: string, variables: Variable // return a rendered mustache template given the specified variables and escape export function renderMustacheString(string: string, variables: Variables, escape: Escape): string { const augmentedVariables = augmentObjectVariables(variables); + const lambdas = getMustacheLambdas(); + const previousMustacheEscape = Mustache.escape; Mustache.escape = getEscape(escape); try { - return Mustache.render(`${string}`, augmentedVariables); + return Mustache.render(`${string}`, { ...lambdas, ...augmentedVariables }); } catch (err) { // log error; the mustache code does not currently leak variables return `error rendering mustache template "${string}": ${err.message}`; @@ -98,6 +102,9 @@ function addToStringDeep(object: unknown): void { // walk arrays, but don't add a toString() as mustache already does something if (Array.isArray(object)) { + // instead, add an asJSON() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (object as any).asJSON = () => JSON.stringify(object); object.forEach((element) => addToStringDeep(element)); return; } diff --git a/x-pack/plugins/actions/tsconfig.json b/x-pack/plugins/actions/tsconfig.json index 77ef11e88bfe3..8c253cb644fee 100644 --- a/x-pack/plugins/actions/tsconfig.json +++ b/x-pack/plugins/actions/tsconfig.json @@ -33,7 +33,8 @@ "@kbn/logging-mocks", "@kbn/core-elasticsearch-client-server-mocks", "@kbn/safer-lodash-set", - "@kbn/core-http-server-mocks" + "@kbn/core-http-server-mocks", + "@kbn/tinymath", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/aiops/public/application/utils/error_utils.ts b/x-pack/plugins/aiops/public/application/utils/error_utils.ts deleted file mode 100644 index f1f1c34dd2959..0000000000000 --- a/x-pack/plugins/aiops/public/application/utils/error_utils.ts +++ /dev/null @@ -1,193 +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. - */ - -// TODO Consolidate with duplicate error utils file in -// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts` - -import type { IHttpFetchError } from '@kbn/core-http-browser'; -import Boom from '@hapi/boom'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; - -export interface WrappedError { - body: { - attributes: { - body: EsErrorBody; - }; - message: Boom.Boom; - }; - statusCode: number; -} - -export interface EsErrorRootCause { - type: string; - reason: string; - caused_by?: EsErrorRootCause; - script?: string; -} - -export interface EsErrorBody { - error: { - root_cause?: EsErrorRootCause[]; - caused_by?: EsErrorRootCause; - type: string; - reason: string; - }; - status: number; -} - -export interface AiOpsResponseError { - statusCode: number; - error: string; - message: string; - attributes?: { - body: EsErrorBody; - }; -} - -export interface ErrorMessage { - message: string; -} - -export interface AiOpsErrorObject { - causedBy?: string; - message: string; - statusCode?: number; - fullError?: EsErrorBody; -} - -export interface AiOpsHttpFetchError extends IHttpFetchError { - body: T; -} - -export type ErrorType = - | WrappedError - | AiOpsHttpFetchError - | EsErrorBody - | Boom.Boom - | string - | undefined; - -export function isEsErrorBody(error: any): error is EsErrorBody { - return error && error.error?.reason !== undefined; -} - -export function isErrorString(error: any): error is string { - return typeof error === 'string'; -} - -export function isErrorMessage(error: any): error is ErrorMessage { - return error && error.message !== undefined && typeof error.message === 'string'; -} - -export function isAiOpsResponseError(error: any): error is AiOpsResponseError { - return typeof error.body === 'object' && 'message' in error.body; -} - -export function isBoomError(error: any): error is Boom.Boom { - return error?.isBoom === true; -} - -export function isWrappedError(error: any): error is WrappedError { - return error && isBoomError(error.body?.message) === true; -} - -export const extractErrorProperties = (error: ErrorType): AiOpsErrorObject => { - // extract properties of the error object from within the response error - // coming from Kibana, Elasticsearch, and our own AiOps messages - - // some responses contain raw es errors as part of a bulk response - // e.g. if some jobs fail the action in a bulk request - - if (isEsErrorBody(error)) { - return { - message: error.error.reason, - statusCode: error.status, - fullError: error, - }; - } - - if (isErrorString(error)) { - return { - message: error, - }; - } - if (isWrappedError(error)) { - return error.body.message?.output?.payload; - } - - if (isBoomError(error)) { - return { - message: error.output.payload.message, - statusCode: error.output.payload.statusCode, - }; - } - - if (error?.body === undefined && !error?.message) { - return { - message: '', - }; - } - - if (typeof error.body === 'string') { - return { - message: error.body, - }; - } - - if (isAiOpsResponseError(error)) { - if ( - typeof error.body.attributes === 'object' && - typeof error.body.attributes.body?.error?.reason === 'string' - ) { - const errObj: AiOpsErrorObject = { - message: error.body.attributes.body.error.reason, - statusCode: error.body.statusCode, - fullError: error.body.attributes.body, - }; - if ( - typeof error.body.attributes.body.error.caused_by === 'object' && - (typeof error.body.attributes.body.error.caused_by?.reason === 'string' || - typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string') - ) { - errObj.causedBy = - error.body.attributes.body.error.caused_by?.caused_by?.reason || - error.body.attributes.body.error.caused_by?.reason; - } - if ( - Array.isArray(error.body.attributes.body.error.root_cause) && - typeof error.body.attributes.body.error.root_cause[0] === 'object' && - isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) - ) { - errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; - errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; - } - return errObj; - } else { - return { - message: error.body.message, - statusCode: error.body.statusCode, - }; - } - } - - if (isErrorMessage(error)) { - return { - message: error.message, - }; - } - - // If all else fail return an empty message instead of JSON.stringify - return { - message: '', - }; -}; - -export const extractErrorMessage = (error: ErrorType): string => { - // extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages - const errorObj = extractErrorProperties(error); - return errorObj.message; -}; diff --git a/x-pack/plugins/aiops/public/application/utils/url_state.ts b/x-pack/plugins/aiops/public/application/utils/url_state.ts new file mode 100644 index 0000000000000..9fdaa443f4c75 --- /dev/null +++ b/x-pack/plugins/aiops/public/application/utils/url_state.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import type { Filter, Query } from '@kbn/es-query'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; + +import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from './search_utils'; + +const defaultSearchQuery = { + match_all: {}, +}; + +export interface AiOpsPageUrlState { + pageKey: 'AIOPS_INDEX_VIEWER'; + pageUrlState: AiOpsIndexBasedAppState; +} + +export interface AiOpsIndexBasedAppState { + searchString?: Query['query']; + searchQuery?: estypes.QueryDslQueryContainer; + searchQueryLanguage: SearchQueryLanguage; + filters?: Filter[]; +} + +export type AiOpsFullIndexBasedAppState = Required; + +export const getDefaultAiOpsListState = ( + overrides?: Partial +): AiOpsFullIndexBasedAppState => ({ + searchString: '', + searchQuery: defaultSearchQuery, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + filters: [], + ...overrides, +}); + +export const isFullAiOpsListState = (arg: unknown): arg is AiOpsFullIndexBasedAppState => { + return isPopulatedObject(arg, Object.keys(getDefaultAiOpsListState())); +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx index ce69ea9fea3ae..b30e3a7d1e6fb 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_app_state.tsx @@ -10,7 +10,6 @@ import { pick } from 'lodash'; import { EuiCallOut } from '@elastic/eui'; -import type { Filter, Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import type { SavedSearch } from '@kbn/discover-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -21,7 +20,6 @@ import { DatePickerContextProvider } from '@kbn/ml-date-picker'; import { UI_SETTINGS } from '@kbn/data-plugin/common'; import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; -import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../application/utils/search_utils'; import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context'; import { AiopsAppContext } from '../../hooks/use_aiops_app_context'; import { DataSourceContext } from '../../hooks/use_data_source'; @@ -42,34 +40,6 @@ export interface ExplainLogRateSpikesAppStateProps { appDependencies: AiopsAppDependencies; } -const defaultSearchQuery = { - match_all: {}, -}; - -export interface AiOpsPageUrlState { - pageKey: 'AIOPS_INDEX_VIEWER'; - pageUrlState: AiOpsIndexBasedAppState; -} - -export interface AiOpsIndexBasedAppState { - searchString?: Query['query']; - searchQuery?: Query['query']; - searchQueryLanguage: SearchQueryLanguage; - filters?: Filter[]; -} - -export const getDefaultAiOpsListState = ( - overrides?: Partial -): Required => ({ - searchString: '', - searchQuery: defaultSearchQuery, - searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, - filters: [], - ...overrides, -}); - -export const restorableDefaults = getDefaultAiOpsListState(); - export const ExplainLogRateSpikesAppState: FC = ({ dataView, savedSearch, diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx index 80640f59901bc..fb9ce01c63391 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_page.tsx @@ -7,6 +7,7 @@ import React, { useCallback, useEffect, useState, FC } from 'react'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiEmptyPrompt, EuiFlexGroup, @@ -28,6 +29,10 @@ import { useDataSource } from '../../hooks/use_data_source'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { SearchQueryLanguage } from '../../application/utils/search_utils'; import { useData } from '../../hooks/use_data'; +import { + getDefaultAiOpsListState, + type AiOpsPageUrlState, +} from '../../application/utils/url_state'; import { DocumentCountContent } from '../document_count_content/document_count_content'; import { SearchPanel } from '../search_panel'; @@ -35,7 +40,6 @@ import type { GroupTableItem } from '../spike_analysis_table/types'; import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider'; import { PageHeader } from '../page_header'; -import { restorableDefaults, type AiOpsPageUrlState } from './explain_log_rate_spikes_app_state'; import { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis'; function getDocumentCountStatsSplitLabel( @@ -66,7 +70,7 @@ export const ExplainLogRateSpikesPage: FC = () => { const [aiopsListState, setAiopsListState] = usePageUrlState( 'AIOPS_INDEX_VIEWER', - restorableDefaults + getDefaultAiOpsListState() ); const [globalState, setGlobalState] = useUrlState('_g'); @@ -80,7 +84,7 @@ export const ExplainLogRateSpikesPage: FC = () => { const setSearchParams = useCallback( (searchParams: { - searchQuery: Query['query']; + searchQuery: estypes.QueryDslQueryContainer; searchString: Query['query']; queryLanguage: SearchQueryLanguage; filters: Filter[]; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx index c888694a7b0c3..747f90d542354 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx @@ -25,7 +25,7 @@ import { import { useDiscoverLinks } from '../use_discover_links'; import { MiniHistogram } from '../../mini_histogram'; import { useEuiTheme } from '../../../hooks/use_eui_theme'; -import type { AiOpsIndexBasedAppState } from '../../explain_log_rate_spikes/explain_log_rate_spikes_app_state'; +import type { AiOpsFullIndexBasedAppState } from '../../../application/utils/url_state'; import type { EventRate, Category, SparkLinesPerCategory } from '../use_categorize_request'; import { useTableState } from './use_table_state'; @@ -42,7 +42,7 @@ interface Props { dataViewId: string; selectedField: string | undefined; timefilter: TimefilterContract; - aiopsListState: Required; + aiopsListState: AiOpsFullIndexBasedAppState; pinnedCategory: Category | null; setPinnedCategory: (category: Category | null) => void; selectedCategory: Category | null; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index b673d8a498a21..7c7d0001aea42 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -4,8 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import React, { FC, useState, useEffect, useCallback, useMemo } from 'react'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiButton, EuiSpacer, @@ -21,14 +23,18 @@ import { import { Filter, Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useUrlState } from '@kbn/ml-url-state'; +import { usePageUrlState, useUrlState } from '@kbn/ml-url-state'; import { useDataSource } from '../../hooks/use_data_source'; import { useData } from '../../hooks/use_data'; import type { SearchQueryLanguage } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; +import { + getDefaultAiOpsListState, + isFullAiOpsListState, + type AiOpsPageUrlState, +} from '../../application/utils/url_state'; -import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state'; import { SearchPanel } from '../search_panel'; import { PageHeader } from '../page_header'; @@ -47,7 +53,10 @@ export const LogCategorizationPage: FC = () => { const { dataView, savedSearch } = useDataSource(); const { runCategorizeRequest, cancelRequest } = useCategorizeRequest(); - const [aiopsListState, setAiopsListState] = useState(restorableDefaults); + const [aiopsListState, setAiopsListState] = usePageUrlState( + 'AIOPS_INDEX_VIEWER', + getDefaultAiOpsListState() + ); const [globalState, setGlobalState] = useUrlState('_g'); const [selectedField, setSelectedField] = useState(); const [selectedCategory, setSelectedCategory] = useState(null); @@ -76,7 +85,7 @@ export const LogCategorizationPage: FC = () => { const setSearchParams = useCallback( (searchParams: { - searchQuery: Query['query']; + searchQuery: estypes.QueryDslQueryContainer; searchString: Query['query']; queryLanguage: SearchQueryLanguage; filters: Filter[]; @@ -289,7 +298,10 @@ export const LogCategorizationPage: FC = () => { fieldSelected={selectedField !== null} /> - {selectedField !== undefined && categories !== null && categories.length > 0 ? ( + {selectedField !== undefined && + categories !== null && + categories.length > 0 && + isFullAiOpsListState(aiopsListState) ? ( = ({ const copyToClipBoardAction = useCopyToClipboardAction(); const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); + const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId); const columns: Array> = [ { @@ -238,7 +240,7 @@ export const SpikeAnalysisTable: FC = ({ name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, copyToClipBoardAction], + actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], width: ACTIONS_COLUMN_WIDTH, valign: 'middle', }, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index b319db0088d4d..ca55b43907bd6 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -40,6 +40,7 @@ import { useSpikeAnalysisTableRowContext } from './spike_analysis_table_row_prov import type { GroupTableItem } from './types'; import { useCopyToClipboardAction } from './use_copy_to_clipboard_action'; import { useViewInDiscoverAction } from './use_view_in_discover_action'; +import { useViewInLogPatternAnalysisAction } from './use_view_in_log_pattern_analysis_action'; const NARROW_COLUMN_WIDTH = '120px'; const EXPAND_COLUMN_WIDTH = '40px'; @@ -121,6 +122,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ const copyToClipBoardAction = useCopyToClipboardAction(); const viewInDiscoverAction = useViewInDiscoverAction(dataViewId); + const viewInLogPatternAnalysisAction = useViewInLogPatternAnalysisAction(dataViewId); const columns: Array> = [ { @@ -355,7 +357,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ name: i18n.translate('xpack.aiops.spikeAnalysisTable.actionsColumnName', { defaultMessage: 'Actions', }), - actions: [viewInDiscoverAction, copyToClipBoardAction], + actions: [viewInDiscoverAction, viewInLogPatternAnalysisAction, copyToClipBoardAction], width: ACTIONS_COLUMN_WIDTH, valign: 'top', }, diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx new file mode 100644 index 0000000000000..16c91f8d3851f --- /dev/null +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/table_action_button.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { type FC } from 'react'; + +import { EuiLink, EuiIcon, EuiText, EuiToolTip, type IconType } from '@elastic/eui'; + +interface TableActionButtonProps { + iconType: IconType; + dataTestSubjPostfix: string; + isDisabled: boolean; + label: string; + tooltipText?: string; + onClick: () => void; +} + +export const TableActionButton: FC = ({ + iconType, + dataTestSubjPostfix, + isDisabled, + label, + tooltipText, + onClick, +}) => { + const buttonContent = ( + <> + + {label} + + ); + + const unwrappedButton = !isDisabled ? ( + + {buttonContent} + + ) : ( + + {buttonContent} + + ); + + if (tooltipText) { + return {unwrappedButton}; + } + + return unwrappedButton; +}; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx index 82359b3d2b1aa..0984c76a4b170 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.test.tsx @@ -30,11 +30,19 @@ describe('useCopyToClipboardAction', () => { it('renders the action for a single significant term', async () => { execCommandMock.mockImplementationOnce(() => true); const { result } = renderHook(() => useCopyToClipboardAction()); - const { getByLabelText } = render((result.current as Action).render(significantTerms[0])); + const { findByText, getByTestId } = render( + (result.current as Action).render(significantTerms[0]) + ); - const button = getByLabelText('Copy field/value pair as KQL syntax to clipboard'); + const button = getByTestId('aiopsTableActionButtonCopyToClipboard enabled'); - expect(button).toBeInTheDocument(); + userEvent.hover(button); + + // The tooltip from EUI takes 250ms to appear, so we must + // use a `find*` query to asynchronously poll for it. + expect( + await findByText('Copy field/value pair as KQL syntax to clipboard') + ).toBeInTheDocument(); await act(async () => { await userEvent.click(button); @@ -50,12 +58,16 @@ describe('useCopyToClipboardAction', () => { it('renders the action for a group of items', async () => { execCommandMock.mockImplementationOnce(() => true); const groupTableItems = getGroupTableItems(finalSignificantTermGroups); - const { result } = renderHook(() => useCopyToClipboardAction()); - const { getByLabelText } = render((result.current as Action).render(groupTableItems[0])); + const { result } = renderHook(useCopyToClipboardAction); + const { findByText, getByText } = render((result.current as Action).render(groupTableItems[0])); + + const button = getByText('Copy to clipboard'); - const button = getByLabelText('Copy group items as KQL syntax to clipboard'); + userEvent.hover(button); - expect(button).toBeInTheDocument(); + // The tooltip from EUI takes 250ms to appear, so we must + // use a `find*` query to asynchronously poll for it. + expect(await findByText('Copy group items as KQL syntax to clipboard')).toBeInTheDocument(); await act(async () => { await userEvent.click(button); diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx index e9924307c1e27..1b906eb56e988 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_copy_to_clipboard_action.tsx @@ -7,14 +7,22 @@ import React from 'react'; -import { EuiCopy, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { EuiCopy, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isSignificantTerm, type SignificantTerm } from '@kbn/ml-agg-utils'; +import { TableActionButton } from './table_action_button'; import { getTableItemAsKQL } from './get_table_item_as_kql'; import type { GroupTableItem, TableItemAction } from './types'; +const copyToClipboardButtonLabel = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardButtonLabel', + { + defaultMessage: 'Copy to clipboard', + } +); + const copyToClipboardSignificantTermMessage = i18n.translate( 'xpack.aiops.spikeAnalysisTable.linksMenu.copyToClipboardSignificantTermMessage', { @@ -37,7 +45,15 @@ export const useCopyToClipboardAction = (): TableItemAction => ({ return ( - {(copy) => } + {(copy) => ( + + )} ); diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx index 5f30abb2f6cec..bd7741bb452bf 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_discover_action.tsx @@ -7,14 +7,13 @@ import React, { useMemo } from 'react'; -import { EuiIcon, EuiToolTip } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; import type { SignificantTerm } from '@kbn/ml-agg-utils'; import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; +import { TableActionButton } from './table_action_button'; import { getTableItemAsKQL } from './get_table_item_as_kql'; import type { GroupTableItem, TableItemAction } from './types'; @@ -83,19 +82,26 @@ export const useViewInDiscoverAction = (dataViewId?: string): TableItemAction => }; return { - name: () => ( - - - - ), - description: viewInDiscoverMessage, - type: 'button', - onClick: async (tableItem) => { - const openInDiscoverUrl = await generateDiscoverUrl(tableItem); - if (typeof openInDiscoverUrl === 'string') { - await application.navigateToUrl(openInDiscoverUrl); - } + render: (tableItem: SignificantTerm | GroupTableItem) => { + const tooltipText = discoverUrlError ? discoverUrlError : viewInDiscoverMessage; + + const clickHandler = async () => { + const openInDiscoverUrl = await generateDiscoverUrl(tableItem); + if (typeof openInDiscoverUrl === 'string') { + await application.navigateToUrl(openInDiscoverUrl); + } + }; + + return ( + + ); }, - enabled: () => discoverUrlError === undefined, }; }; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx new file mode 100644 index 0000000000000..9388cf147c8ff --- /dev/null +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/use_view_in_log_pattern_analysis_action.tsx @@ -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 React, { useMemo } from 'react'; + +import { SerializableRecord } from '@kbn/utility-types'; +import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import type { SignificantTerm } from '@kbn/ml-agg-utils'; + +import { SEARCH_QUERY_LANGUAGE } from '../../application/utils/search_utils'; +import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; + +import { TableActionButton } from './table_action_button'; +import { getTableItemAsKQL } from './get_table_item_as_kql'; +import type { GroupTableItem, TableItemAction } from './types'; + +const viewInLogPatternAnalysisMessage = i18n.translate( + 'xpack.aiops.spikeAnalysisTable.linksMenu.viewInLogPatternAnalysis', + { + defaultMessage: 'View in Log Pattern Analysis', + } +); + +export const useViewInLogPatternAnalysisAction = (dataViewId?: string): TableItemAction => { + const { application, share, data } = useAiopsAppContext(); + + const mlLocator = useMemo(() => share.url.locators.get('ML_APP_LOCATOR'), [share.url.locators]); + + const generateLogPatternAnalysisUrl = async ( + groupTableItem: GroupTableItem | SignificantTerm + ) => { + if (mlLocator !== undefined) { + const searchString = getTableItemAsKQL(groupTableItem); + const ast = fromKueryExpression(searchString); + const searchQuery = toElasticsearchQuery(ast); + + const appState = { + AIOPS_INDEX_VIEWER: { + filters: data.query.filterManager.getFilters(), + // QueryDslQueryContainer type triggers an error as being + // not working with SerializableRecord, however, it works as expected. + searchQuery: searchQuery as unknown, + searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY, + searchString: getTableItemAsKQL(groupTableItem), + }, + } as SerializableRecord; + + return await mlLocator.getUrl({ + page: 'aiops/log_categorization', + pageState: { + index: dataViewId, + timeRange: data.query.timefilter.timefilter.getTime(), + appState, + }, + }); + } + }; + + const logPatternAnalysisUrlError = useMemo(() => { + if (!mlLocator) { + return i18n.translate('xpack.aiops.spikeAnalysisTable.mlLocatorMissingErrorMessage', { + defaultMessage: 'No locator for Log Pattern Analysis detected', + }); + } + if (!dataViewId) { + return i18n.translate( + 'xpack.aiops.spikeAnalysisTable.autoGeneratedLogPatternAnalysisLinkErrorMessage', + { + defaultMessage: + 'Unable to link to Log Pattern Analysis; no data view exists for this index', + } + ); + } + }, [dataViewId, mlLocator]); + + return { + render: (tableItem: SignificantTerm | GroupTableItem) => { + const message = logPatternAnalysisUrlError + ? logPatternAnalysisUrlError + : viewInLogPatternAnalysisMessage; + + const clickHandler = async () => { + const openInLogPatternAnalysisUrl = await generateLogPatternAnalysisUrl(tableItem); + if (typeof openInLogPatternAnalysisUrl === 'string') { + await application.navigateToUrl(openInLogPatternAnalysisUrl); + } + }; + + const isDisabled = logPatternAnalysisUrlError !== undefined; + + return ( + + ); + }, + }; +}; diff --git a/x-pack/plugins/aiops/public/hooks/use_data.ts b/x-pack/plugins/aiops/public/hooks/use_data.ts index c390582ccae1a..62f4c596cc60e 100644 --- a/x-pack/plugins/aiops/public/hooks/use_data.ts +++ b/x-pack/plugins/aiops/public/hooks/use_data.ts @@ -18,7 +18,7 @@ import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker'; import { PLUGIN_ID } from '../../common'; import type { DocumentStatsSearchStrategyParams } from '../get_document_stats'; -import type { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_app_state'; +import type { AiOpsIndexBasedAppState } from '../application/utils/url_state'; import { getEsQueryFromSavedSearch } from '../application/utils/search_utils'; import type { GroupTableItem } from '../components/spike_analysis_table/types'; diff --git a/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts b/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts index 9e4f50368a0b5..8cfaa074286d6 100644 --- a/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts +++ b/x-pack/plugins/aiops/public/hooks/use_document_count_stats.ts @@ -12,10 +12,10 @@ import { i18n } from '@kbn/i18n'; import type { ToastsStart } from '@kbn/core/public'; import { stringHash } from '@kbn/ml-string-hash'; import { createRandomSamplerWrapper } from '@kbn/ml-random-sampler-utils'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { RANDOM_SAMPLER_SEED } from '../../common/constants'; -import { extractErrorProperties } from '../application/utils/error_utils'; import { DocumentCountStats, getDocumentCountStatsRequest, diff --git a/x-pack/plugins/aiops/tsconfig.json b/x-pack/plugins/aiops/tsconfig.json index b9a6ff5408eda..6c9aafffa26d0 100644 --- a/x-pack/plugins/aiops/tsconfig.json +++ b/x-pack/plugins/aiops/tsconfig.json @@ -34,7 +34,6 @@ "@kbn/ui-theme", "@kbn/i18n-react", "@kbn/rison", - "@kbn/core-http-browser", "@kbn/aiops-components", "@kbn/aiops-utils", "@kbn/licensing-plugin", @@ -51,6 +50,8 @@ "@kbn/ml-route-utils", "@kbn/unified-field-list-plugin", "@kbn/ml-random-sampler-utils", + "@kbn/utility-types", + "@kbn/ml-error-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/alerting/common/alert_summary.ts b/x-pack/plugins/alerting/common/alert_summary.ts index b1563882d4f21..ed6cf325e20b1 100644 --- a/x-pack/plugins/alerting/common/alert_summary.ts +++ b/x-pack/plugins/alerting/common/alert_summary.ts @@ -29,6 +29,7 @@ export interface AlertSummary { errorMessages: Array<{ date: string; message: string }>; alerts: Record; executionDuration: ExecutionDuration; + revision: number; } export interface AlertStatus { @@ -38,4 +39,5 @@ export interface AlertStatus { actionGroupId?: string; activeStartDate?: string; flapping: boolean; + maintenanceWindowIds?: string[]; } diff --git a/x-pack/plugins/alerting/common/execution_log_types.ts b/x-pack/plugins/alerting/common/execution_log_types.ts index 2d5e34df8f766..7a35bea0df619 100644 --- a/x-pack/plugins/alerting/common/execution_log_types.ts +++ b/x-pack/plugins/alerting/common/execution_log_types.ts @@ -63,6 +63,7 @@ export interface IExecutionLog { rule_id: string; space_ids: string[]; rule_name: string; + maintenance_window_ids: string[]; } export interface IExecutionErrors { diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index 0bac3fd995a27..f58324f1d09ee 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -10,7 +10,7 @@ import type { SavedObjectAttributes, SavedObjectsResolveResponse, } from '@kbn/core/server'; -import type { KueryNode } from '@kbn/es-query'; +import type { Filter, KueryNode } from '@kbn/es-query'; import { RuleNotifyWhenType } from './rule_notify_when_type'; import { RuleSnooze } from './rule_snooze_type'; @@ -94,11 +94,12 @@ export interface AlertsFilterTimeframe extends SavedObjectAttributes { } export interface AlertsFilter extends SavedObjectAttributes { - query: null | { + query?: { kql: string; + filters: Filter[]; dsl?: string; // This fields is generated in the code by using "kql", therefore it's not optional but defined as optional to avoid modifying a lot of files in different plugins }; - timeframe: null | AlertsFilterTimeframe; + timeframe?: AlertsFilterTimeframe; } export type RuleActionAlertsFilterProperty = AlertsFilterTimeframe | RuleActionParam; @@ -191,10 +192,11 @@ export interface Rule { } export interface SanitizedAlertsFilter extends AlertsFilter { - query: null | { + query?: { kql: string; + filters: Filter[]; }; - timeframe: null | AlertsFilterTimeframe; + timeframe?: AlertsFilterTimeframe; } export type SanitizedRuleAction = Omit & { diff --git a/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx new file mode 100644 index 0000000000000..e6bd2a4071b27 --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.test.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor } from '@testing-library/dom'; + +import { MaintenanceWindow } from '../pages/maintenance_windows/types'; +import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; +import { useArchiveMaintenanceWindow } from './use_archive_maintenance_window'; + +const mockAddDanger = jest.fn(); +const mockAddSuccess = jest.fn(); + +jest.mock('../utils/kibana_react', () => { + const originalModule = jest.requireActual('../utils/kibana_react'); + return { + ...originalModule, + useKibana: () => { + const { services } = originalModule.useKibana(); + return { + services: { + ...services, + notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } }, + }, + }; + }, + }; +}); +jest.mock('../services/maintenance_windows_api/archive', () => ({ + archiveMaintenanceWindow: jest.fn(), +})); + +const { archiveMaintenanceWindow } = jest.requireMock( + '../services/maintenance_windows_api/archive' +); + +const maintenanceWindow: MaintenanceWindow = { + title: 'archive', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + }, +}; + +let appMockRenderer: AppMockRenderer; + +describe('useArchiveMaintenanceWindow', () => { + beforeEach(() => { + jest.clearAllMocks(); + + appMockRenderer = createAppMockRenderer(); + archiveMaintenanceWindow.mockResolvedValue(maintenanceWindow); + }); + + it('should call onSuccess if api succeeds', async () => { + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: true }); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith("Archived maintenance window 'archive'") + ); + }); + + it('should call onError if api fails', async () => { + archiveMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: true }); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to archive maintenance window.') + ); + }); + + it('should call onSuccess if api succeeds (unarchive)', async () => { + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: false }); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith("Unarchived maintenance window 'archive'") + ); + }); + + it('should call onError if api fails (unarchive)', async () => { + archiveMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate({ maintenanceWindowId: '123', archive: false }); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to unarchive maintenance window.') + ); + }); +}); diff --git a/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.ts new file mode 100644 index 0000000000000..2bda74f83b9bf --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_archive_maintenance_window.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMutation } from '@tanstack/react-query'; + +import { useKibana } from '../utils/kibana_react'; +import { archiveMaintenanceWindow } from '../services/maintenance_windows_api/archive'; + +export function useArchiveMaintenanceWindow() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const mutationFn = ({ + maintenanceWindowId, + archive, + }: { + maintenanceWindowId: string; + archive: boolean; + }) => { + return archiveMaintenanceWindow({ http, maintenanceWindowId, archive }); + }; + + return useMutation(mutationFn, { + onSuccess: (data, { archive }) => { + const archiveToast = i18n.translate('xpack.alerting.maintenanceWindowsArchiveSuccess', { + defaultMessage: "Archived maintenance window '{title}'", + values: { + title: data.title, + }, + }); + const unarchiveToast = i18n.translate('xpack.alerting.maintenanceWindowsUnarchiveSuccess', { + defaultMessage: "Unarchived maintenance window '{title}'", + values: { + title: data.title, + }, + }); + toasts.addSuccess(archive ? archiveToast : unarchiveToast); + }, + onError: (error, { archive }) => { + const archiveToast = i18n.translate('xpack.alerting.maintenanceWindowsArchiveFailure', { + defaultMessage: 'Failed to archive maintenance window.', + }); + const unarchiveToast = i18n.translate('xpack.alerting.maintenanceWindowsUnarchiveFailure', { + defaultMessage: 'Failed to unarchive maintenance window.', + }); + toasts.addDanger(archive ? archiveToast : unarchiveToast); + }, + }); +} diff --git a/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts index 08c01bb080055..e710595bc6180 100644 --- a/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts +++ b/x-pack/plugins/alerting/public/hooks/use_create_maintenance_window.ts @@ -23,12 +23,12 @@ export function useCreateMaintenanceWindow() { }; return useMutation(mutationFn, { - onSuccess: (variables: MaintenanceWindow) => { + onSuccess: (data) => { toasts.addSuccess( i18n.translate('xpack.alerting.maintenanceWindowsCreateSuccess', { defaultMessage: "Created maintenance window '{title}'", values: { - title: variables.title, + title: data.title, }, }) ); diff --git a/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts b/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts index 6a71bd9c64518..10b7f3402aca1 100644 --- a/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts +++ b/x-pack/plugins/alerting/public/hooks/use_find_maintenance_windows.ts @@ -30,7 +30,11 @@ export const useFindMaintenanceWindows = () => { } }; - const { isLoading, data = [] } = useQuery({ + const { + isLoading, + data = [], + refetch, + } = useQuery({ queryKey: ['findMaintenanceWindows'], queryFn, onError: onErrorFn, @@ -42,5 +46,6 @@ export const useFindMaintenanceWindows = () => { return { maintenanceWindows: data, isLoading, + refetch, }; }; diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx new file mode 100644 index 0000000000000..b80dbbae355bc --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.test.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor } from '@testing-library/dom'; + +import { MaintenanceWindow } from '../pages/maintenance_windows/types'; +import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; +import { useFinishAndArchiveMaintenanceWindow } from './use_finish_and_archive_maintenance_window'; + +const mockAddDanger = jest.fn(); +const mockAddSuccess = jest.fn(); + +jest.mock('../utils/kibana_react', () => { + const originalModule = jest.requireActual('../utils/kibana_react'); + return { + ...originalModule, + useKibana: () => { + const { services } = originalModule.useKibana(); + return { + services: { + ...services, + notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } }, + }, + }; + }, + }; +}); +jest.mock('../services/maintenance_windows_api/finish', () => ({ + finishMaintenanceWindow: jest.fn(), +})); +jest.mock('../services/maintenance_windows_api/archive', () => ({ + archiveMaintenanceWindow: jest.fn(), +})); + +const { finishMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/finish'); +const { archiveMaintenanceWindow } = jest.requireMock( + '../services/maintenance_windows_api/archive' +); + +const maintenanceWindow: MaintenanceWindow = { + title: 'test', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + }, +}; + +let appMockRenderer: AppMockRenderer; + +describe('useFinishAndArchiveMaintenanceWindow', () => { + beforeEach(() => { + jest.clearAllMocks(); + + appMockRenderer = createAppMockRenderer(); + finishMaintenanceWindow.mockResolvedValue(maintenanceWindow); + archiveMaintenanceWindow.mockResolvedValue(maintenanceWindow); + }); + + it('should call onSuccess if api succeeds', async () => { + const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith( + "Cancelled and archived running maintenance window 'test'" + ) + ); + }); + + it('should call onError if finish api fails', async () => { + finishMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to cancel and archive maintenance window.') + ); + }); + + it('should call onError if archive api fails', async () => { + archiveMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useFinishAndArchiveMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to cancel and archive maintenance window.') + ); + }); +}); diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.ts new file mode 100644 index 0000000000000..d68bf2c89e379 --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_and_archive_maintenance_window.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMutation } from '@tanstack/react-query'; + +import { useKibana } from '../utils/kibana_react'; +import { finishMaintenanceWindow } from '../services/maintenance_windows_api/finish'; +import { archiveMaintenanceWindow } from '../services/maintenance_windows_api/archive'; + +export function useFinishAndArchiveMaintenanceWindow() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const mutationFn = async (maintenanceWindowId: string) => { + await finishMaintenanceWindow({ http, maintenanceWindowId }); + return archiveMaintenanceWindow({ http, maintenanceWindowId, archive: true }); + }; + + return useMutation(mutationFn, { + onSuccess: (data) => { + toasts.addSuccess( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedAndArchiveSuccess', { + defaultMessage: "Cancelled and archived running maintenance window '{title}'", + values: { + title: data.title, + }, + }) + ); + }, + onError: () => { + toasts.addDanger( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedAndArchiveFailure', { + defaultMessage: 'Failed to cancel and archive maintenance window.', + }) + ); + }, + }); +} diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx new file mode 100644 index 0000000000000..ed534cb835c8d --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { act, renderHook } from '@testing-library/react-hooks/dom'; +import { waitFor } from '@testing-library/dom'; + +import { MaintenanceWindow } from '../pages/maintenance_windows/types'; +import { AppMockRenderer, createAppMockRenderer } from '../lib/test_utils'; +import { useFinishMaintenanceWindow } from './use_finish_maintenance_window'; + +const mockAddDanger = jest.fn(); +const mockAddSuccess = jest.fn(); + +jest.mock('../utils/kibana_react', () => { + const originalModule = jest.requireActual('../utils/kibana_react'); + return { + ...originalModule, + useKibana: () => { + const { services } = originalModule.useKibana(); + return { + services: { + ...services, + notifications: { toasts: { addSuccess: mockAddSuccess, addDanger: mockAddDanger } }, + }, + }; + }, + }; +}); +jest.mock('../services/maintenance_windows_api/finish', () => ({ + finishMaintenanceWindow: jest.fn(), +})); + +const { finishMaintenanceWindow } = jest.requireMock('../services/maintenance_windows_api/finish'); + +const maintenanceWindow: MaintenanceWindow = { + title: 'cancel', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + }, +}; + +let appMockRenderer: AppMockRenderer; + +describe('useFinishMaintenanceWindow', () => { + beforeEach(() => { + jest.clearAllMocks(); + + appMockRenderer = createAppMockRenderer(); + finishMaintenanceWindow.mockResolvedValue(maintenanceWindow); + }); + + it('should call onSuccess if api succeeds', async () => { + const { result } = renderHook(() => useFinishMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + await waitFor(() => + expect(mockAddSuccess).toBeCalledWith("Cancelled running maintenance window 'cancel'") + ); + }); + + it('should call onError if api fails', async () => { + finishMaintenanceWindow.mockRejectedValue(''); + + const { result } = renderHook(() => useFinishMaintenanceWindow(), { + wrapper: appMockRenderer.AppWrapper, + }); + + await act(async () => { + await result.current.mutate('123'); + }); + + await waitFor(() => + expect(mockAddDanger).toBeCalledWith('Failed to cancel maintenance window.') + ); + }); +}); diff --git a/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.ts new file mode 100644 index 0000000000000..7e8aafa1793ad --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_finish_maintenance_window.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { useMutation } from '@tanstack/react-query'; + +import { useKibana } from '../utils/kibana_react'; +import { finishMaintenanceWindow } from '../services/maintenance_windows_api/finish'; + +export function useFinishMaintenanceWindow() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const mutationFn = (maintenanceWindowId: string) => { + return finishMaintenanceWindow({ http, maintenanceWindowId }); + }; + + return useMutation(mutationFn, { + onSuccess: (data) => { + toasts.addSuccess( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedSuccess', { + defaultMessage: "Cancelled running maintenance window '{title}'", + values: { + title: data.title, + }, + }) + ); + }, + onError: () => { + toasts.addDanger( + i18n.translate('xpack.alerting.maintenanceWindowsFinishedFailure', { + defaultMessage: 'Failed to cancel maintenance window.', + }) + ); + }, + }); +} diff --git a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx index 67545d83aba17..897b44295d8c0 100644 --- a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx +++ b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.test.tsx @@ -79,7 +79,7 @@ describe('useUpdateMaintenanceWindow', () => { }); await waitFor(() => - expect(mockAddDanger).toBeCalledWith("Failed to update maintenance window '123'") + expect(mockAddDanger).toBeCalledWith('Failed to update maintenance window.') ); }); }); diff --git a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts index de6596b1c766d..c7dd73724b6df 100644 --- a/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts +++ b/x-pack/plugins/alerting/public/hooks/use_update_maintenance_window.ts @@ -39,13 +39,10 @@ export function useUpdateMaintenanceWindow() { }) ); }, - onError: (error, variables) => { + onError: () => { toasts.addDanger( i18n.translate('xpack.alerting.maintenanceWindowsUpdateFailure', { - defaultMessage: "Failed to update maintenance window '{id}'", - values: { - id: variables.maintenanceWindowId, - }, + defaultMessage: 'Failed to update maintenance window.', }) ); }, diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx index b3fe479c21a88..0dda6a8890529 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import moment from 'moment'; import { FIELD_TYPES, @@ -16,7 +16,10 @@ import { } from '@kbn/es-ui-shared-plugin/static/forms/hook_form_lib'; import { Field } from '@kbn/es-ui-shared-plugin/static/forms/components'; import { + EuiButton, EuiButtonEmpty, + EuiCallOut, + EuiConfirmModal, EuiFlexGroup, EuiFlexItem, EuiFormLabel, @@ -33,6 +36,7 @@ import { useCreateMaintenanceWindow } from '../../../hooks/use_create_maintenanc import { useUpdateMaintenanceWindow } from '../../../hooks/use_update_maintenance_window'; import { useUiSetting } from '../../../utils/kibana_react'; import { DatePickerRangeField } from './fields/date_picker_range_field'; +import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window'; const UseField = getUseField({ component: Field }); @@ -56,6 +60,7 @@ export const CreateMaintenanceWindowForm = React.memo { const [defaultStartDateValue] = useState(moment().toISOString()); const [defaultEndDateValue] = useState(moment().add(30, 'minutes').toISOString()); + const [isModalVisible, setIsModalVisible] = useState(false); const { defaultTimezone, isBrowser } = useDefaultTimezone(); const isEditMode = initialValue !== undefined && maintenanceWindowId !== undefined; @@ -63,6 +68,7 @@ export const CreateMaintenanceWindowForm = React.memo { @@ -109,6 +115,35 @@ export const CreateMaintenanceWindowForm = React.memo setIsModalVisible(false), []); + const showModal = useCallback(() => setIsModalVisible(true), []); + + const modal = useMemo(() => { + let m; + if (isModalVisible) { + m = ( + { + closeModal(); + archiveMaintenanceWindow( + { maintenanceWindowId: maintenanceWindowId!, archive: true }, + { onSuccess } + ); + }} + cancelButtonText={i18n.CANCEL} + confirmButtonText={i18n.ARCHIVE_TITLE} + defaultFocusedButton="confirm" + buttonColor="danger" + > +

{i18n.ARCHIVE_CALLOUT_SUBTITLE}

+
+ ); + } + return m; + }, [closeModal, archiveMaintenanceWindow, isModalVisible, maintenanceWindowId, onSuccess]); + return (
@@ -192,6 +227,15 @@ export const CreateMaintenanceWindowForm = React.memo : null} + {isEditMode ? ( + +

{i18n.ARCHIVE_SUBTITLE}

+ + {i18n.ARCHIVE} + + {modal} +
+ ) : null} { }); test('it renders', () => { - const result = appMockRenderer.render(); + const result = appMockRenderer.render( + {}} loading={false} items={items} /> + ); expect(result.getAllByTestId('list-item')).toHaveLength(items.length); diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx index 9d4fc521c3f66..705219b9baa2a 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_windows_list.tsx @@ -5,29 +5,34 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { formatDate, EuiInMemoryTable, EuiBasicTableColumn, - EuiButton, - useEuiBackgroundColor, EuiFlexGroup, EuiFlexItem, SearchFilterConfig, + EuiBadge, + useEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; import { MaintenanceWindowFindResponse, SortDirection } from '../types'; import * as i18n from '../translations'; import { useEditMaintenanceWindowsNavigation } from '../../../hooks/use_navigation'; +import { STATUS_DISPLAY, STATUS_SORT } from '../constants'; import { UpcomingEventsPopover } from './upcoming_events_popover'; -import { StatusColor, STATUS_DISPLAY, STATUS_SORT } from '../constants'; import { MaintenanceWindowStatus } from '../../../../common'; import { StatusFilter } from './status_filter'; +import { TableActionsPopover } from './table_actions_popover'; +import { useFinishMaintenanceWindow } from '../../../hooks/use_finish_maintenance_window'; +import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window'; +import { useFinishAndArchiveMaintenanceWindow } from '../../../hooks/use_finish_and_archive_maintenance_window'; interface MaintenanceWindowsListProps { loading: boolean; items: MaintenanceWindowFindResponse[]; + refreshData: () => void; } const columns: Array> = [ @@ -39,23 +44,9 @@ const columns: Array> = [ { field: 'status', name: i18n.TABLE_STATUS, - render: (status: string) => { + render: (status: MaintenanceWindowStatus) => { return ( - {}} - > - {STATUS_DISPLAY[status].label} - + {STATUS_DISPLAY[status].label} ); }, sortable: ({ status }) => STATUS_SORT[status], @@ -108,38 +99,61 @@ const search: { filters: SearchFilterConfig[] } = { }; export const MaintenanceWindowsList = React.memo( - ({ loading, items }) => { + ({ loading, items, refreshData }) => { + const { euiTheme } = useEuiTheme(); const { navigateToEditMaintenanceWindows } = useEditMaintenanceWindowsNavigation(); - const warningBackgroundColor = useEuiBackgroundColor('warning'); - const subduedBackgroundColor = useEuiBackgroundColor('subdued'); + const onEdit = useCallback( + (id) => navigateToEditMaintenanceWindows(id), + [navigateToEditMaintenanceWindows] + ); + const { mutate: finishMaintenanceWindow, isLoading: isLoadingFinish } = + useFinishMaintenanceWindow(); + const onCancel = useCallback( + (id) => finishMaintenanceWindow(id, { onSuccess: () => refreshData() }), + [finishMaintenanceWindow, refreshData] + ); + const { mutate: archiveMaintenanceWindow, isLoading: isLoadingArchive } = + useArchiveMaintenanceWindow(); + const onArchive = useCallback( + (id: string, archive: boolean) => + archiveMaintenanceWindow( + { maintenanceWindowId: id, archive }, + { onSuccess: () => refreshData() } + ), + [archiveMaintenanceWindow, refreshData] + ); + const { mutate: finishAndArchiveMaintenanceWindow, isLoading: isLoadingFinishAndArchive } = + useFinishAndArchiveMaintenanceWindow(); + const onCancelAndArchive = useCallback( + (id: string) => finishAndArchiveMaintenanceWindow(id, { onSuccess: () => refreshData() }), + [finishAndArchiveMaintenanceWindow, refreshData] + ); + const tableCss = useMemo(() => { return css` .euiTableRow { &.running { - background-color: ${warningBackgroundColor}; - } - - &.archived { - background-color: ${subduedBackgroundColor}; + background-color: ${euiTheme.colors.highlight}; } } `; - }, [warningBackgroundColor, subduedBackgroundColor]); + }, [euiTheme.colors.highlight]); const actions: Array> = [ { name: '', - actions: [ - { - name: i18n.TABLE_ACTION_EDIT, - isPrimary: true, - description: 'Edit maintenance window', - icon: 'pencil', - type: 'icon', - onClick: (mw: MaintenanceWindowFindResponse) => navigateToEditMaintenanceWindows(mw.id), - 'data-test-subj': 'action-edit', - }, - ], + render: ({ status, id }: { status: MaintenanceWindowStatus; id: string }) => { + return ( + + ); + }, }, ]; @@ -147,7 +161,7 @@ export const MaintenanceWindowsList = React.memo( { + let appMockRenderer: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); + }); + + test('it renders', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + + expect(result.getByTestId('table-actions-icon-button')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is running', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-cancel')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-cancel-and-archive')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is upcoming', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-archive')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is finished', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-edit')).toBeInTheDocument(); + expect(result.getByTestId('table-actions-archive')).toBeInTheDocument(); + }); + + test('it shows the correct actions when a maintenance window is archived', () => { + const result = appMockRenderer.render( + {}} + onCancel={() => {}} + onArchive={() => {}} + onCancelAndArchive={() => {}} + /> + ); + fireEvent.click(result.getByTestId('table-actions-icon-button')); + expect(result.getByTestId('table-actions-unarchive')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.tsx new file mode 100644 index 0000000000000..4742ede93d53c --- /dev/null +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/table_actions_popover.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, { useCallback, useMemo, useState } from 'react'; +import { + EuiButtonIcon, + EuiConfirmModal, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, +} from '@elastic/eui'; +import * as i18n from '../translations'; +import { MaintenanceWindowStatus } from '../../../../common'; + +interface TableActionsPopoverProps { + id: string; + status: MaintenanceWindowStatus; + onEdit: (id: string) => void; + onCancel: (id: string) => void; + onArchive: (id: string, archive: boolean) => void; + onCancelAndArchive: (id: string) => void; +} +type ModalType = 'cancel' | 'cancelAndArchive' | 'archive' | 'unarchive'; +type ActionType = ModalType | 'edit'; + +export const TableActionsPopover: React.FC = React.memo( + ({ id, status, onEdit, onCancel, onArchive, onCancelAndArchive }) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [modalType, setModalType] = useState(); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen((open) => !open); + }, []); + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const closeModal = useCallback(() => setIsModalVisible(false), []); + const showModal = useCallback((type: ModalType) => { + setModalType(type); + setIsModalVisible(true); + }, []); + + const modal = useMemo(() => { + const modals = { + cancel: { + props: { + title: i18n.CANCEL_MODAL_TITLE, + onConfirm: () => { + closeModal(); + onCancel(id); + }, + cancelButtonText: i18n.CANCEL_MODAL_BUTTON, + confirmButtonText: i18n.CANCEL_MODAL_TITLE, + }, + subtitle: i18n.CANCEL_MODAL_SUBTITLE, + }, + cancelAndArchive: { + props: { + title: i18n.CANCEL_AND_ARCHIVE_MODAL_TITLE, + onConfirm: () => { + closeModal(); + onCancelAndArchive(id); + }, + cancelButtonText: i18n.CANCEL_MODAL_BUTTON, + confirmButtonText: i18n.CANCEL_AND_ARCHIVE_MODAL_TITLE, + }, + subtitle: i18n.CANCEL_AND_ARCHIVE_MODAL_SUBTITLE, + }, + archive: { + props: { + title: i18n.ARCHIVE_TITLE, + onConfirm: () => { + closeModal(); + onArchive(id, true); + }, + cancelButtonText: i18n.CANCEL, + confirmButtonText: i18n.ARCHIVE_TITLE, + }, + subtitle: i18n.ARCHIVE_SUBTITLE, + }, + unarchive: { + props: { + title: i18n.UNARCHIVE_MODAL_TITLE, + onConfirm: () => { + closeModal(); + onArchive(id, false); + }, + cancelButtonText: i18n.CANCEL, + confirmButtonText: i18n.UNARCHIVE_MODAL_TITLE, + }, + subtitle: i18n.UNARCHIVE_MODAL_SUBTITLE, + }, + }; + let m; + if (isModalVisible && modalType) { + const modalProps = modals[modalType]; + m = ( + +

{modalProps.subtitle}

+
+ ); + } + return m; + }, [id, modalType, isModalVisible, closeModal, onArchive, onCancel, onCancelAndArchive]); + + const items = useMemo(() => { + const menuItems = { + edit: ( + { + closePopover(); + onEdit(id); + }} + > + {i18n.TABLE_ACTION_EDIT} + + ), + cancel: ( + { + closePopover(); + showModal('cancel'); + }} + > + {i18n.TABLE_ACTION_CANCEL} + + ), + cancelAndArchive: ( + { + closePopover(); + showModal('cancelAndArchive'); + }} + > + {i18n.TABLE_ACTION_CANCEL_AND_ARCHIVE} + + ), + archive: ( + { + closePopover(); + showModal('archive'); + }} + > + {i18n.ARCHIVE} + + ), + unarchive: ( + { + closePopover(); + showModal('unarchive'); + }} + > + {i18n.TABLE_ACTION_UNARCHIVE} + + ), + }; + const statusMenuItemsMap: Record = { + running: ['edit', 'cancel', 'cancelAndArchive'], + upcoming: ['edit', 'archive'], + finished: ['edit', 'archive'], + archived: ['unarchive'], + }; + return statusMenuItemsMap[status].map((type) => menuItems[type]); + }, [id, status, onEdit, closePopover, showModal]); + + const button = useMemo( + () => ( + + ), + [onButtonClick] + ); + + return ( + <> + + + + + + + + {modal} + + ); + } +); +TableActionsPopover.displayName = 'TableActionsPopover'; diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts index 1aed4dac0568c..27b10e804693d 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/constants.ts @@ -107,15 +107,13 @@ export const RRULE_WEEKDAYS_TO_ISO_WEEKDAYS = mapValues(invert(ISO_WEEKDAYS_TO_R Number(v) ); -export const STATUS_DISPLAY: Record = { - [MaintenanceWindowStatus.Running]: { color: 'warning', label: i18n.TABLE_STATUS_RUNNING }, +export const STATUS_DISPLAY = { + [MaintenanceWindowStatus.Running]: { color: 'primary', label: i18n.TABLE_STATUS_RUNNING }, [MaintenanceWindowStatus.Upcoming]: { color: 'warning', label: i18n.TABLE_STATUS_UPCOMING }, [MaintenanceWindowStatus.Finished]: { color: 'success', label: i18n.TABLE_STATUS_FINISHED }, - [MaintenanceWindowStatus.Archived]: { color: 'text', label: i18n.TABLE_STATUS_ARCHIVED }, + [MaintenanceWindowStatus.Archived]: { color: 'default', label: i18n.TABLE_STATUS_ARCHIVED }, }; -export type StatusColor = 'warning' | 'success' | 'text'; - export const STATUS_SORT = { [MaintenanceWindowStatus.Running]: 0, [MaintenanceWindowStatus.Upcoming]: 1, diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx index ab5828f0dffa3..fa9b54122562d 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/index.tsx @@ -31,7 +31,7 @@ export const MaintenanceWindowsPage = React.memo(() => { const { docLinks } = useKibana().services; const { navigateToCreateMaintenanceWindow } = useCreateMaintenanceWindowNavigation(); - const { isLoading, maintenanceWindows } = useFindMaintenanceWindows(); + const { isLoading, maintenanceWindows, refetch } = useFindMaintenanceWindows(); useBreadcrumbs(AlertingDeepLinkId.maintenanceWindows); @@ -39,6 +39,8 @@ export const MaintenanceWindowsPage = React.memo(() => { navigateToCreateMaintenanceWindow(); }, [navigateToCreateMaintenanceWindow]); + const refreshData = useCallback(() => refetch(), [refetch]); + const showEmptyPrompt = !isLoading && maintenanceWindows.length === 0; if (isLoading) { @@ -77,7 +79,11 @@ export const MaintenanceWindowsPage = React.memo(() => { ) : ( <> - + )} diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts index 30b83963cc5b7..65f24411a2a1a 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts @@ -454,6 +454,102 @@ export const SAVE_MAINTENANCE_WINDOW = i18n.translate( } ); +export const TABLE_ACTION_CANCEL = i18n.translate( + 'xpack.alerting.maintenanceWindows.table.cancel', + { + defaultMessage: 'Cancel', + } +); + +export const CANCEL_MODAL_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelModal.title', + { + defaultMessage: 'Cancel maintenance window', + } +); + +export const CANCEL_MODAL_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelModal.subtitle', + { + defaultMessage: + 'Rule notifications resume immediately. Running maintenance window events are canceled; upcoming events are unaffected.', + } +); + +export const CANCEL_MODAL_BUTTON = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelModal.button', + { + defaultMessage: 'Keep running', + } +); + +export const TABLE_ACTION_CANCEL_AND_ARCHIVE = i18n.translate( + 'xpack.alerting.maintenanceWindows.table.cancelAndArchive', + { + defaultMessage: 'Cancel and archive', + } +); + +export const CANCEL_AND_ARCHIVE_MODAL_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelAndArchiveModal.title', + { + defaultMessage: 'Cancel and archive maintenance window', + } +); + +export const CANCEL_AND_ARCHIVE_MODAL_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.cancelAndArchiveModal.subtitle', + { + defaultMessage: + 'Rule notifications resume immediately. All running and upcoming maintenance window events are canceled and the window is queued for deletion.', + } +); + +export const ARCHIVE = i18n.translate('xpack.alerting.maintenanceWindows.archive', { + defaultMessage: 'Archive', +}); + +export const ARCHIVE_TITLE = i18n.translate('xpack.alerting.maintenanceWindows.archive.title', { + defaultMessage: 'Archive maintenance window', +}); + +export const ARCHIVE_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.archive.subtitle', + { + defaultMessage: + 'Upcoming maintenance window events are canceled and the window is queued for deletion.', + } +); + +export const TABLE_ACTION_UNARCHIVE = i18n.translate( + 'xpack.alerting.maintenanceWindows.table.unarchive', + { + defaultMessage: 'Unarchive', + } +); + +export const UNARCHIVE_MODAL_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.unarchiveModal.title', + { + defaultMessage: 'Unarchive maintenance window', + } +); + +export const UNARCHIVE_MODAL_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.unarchiveModal.subtitle', + { + defaultMessage: 'Upcoming maintenance window events are scheduled.', + } +); + +export const ARCHIVE_CALLOUT_SUBTITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.archiveCallout.subtitle', + { + defaultMessage: + 'The changes you have made here will not be saved. Are you sure you want to discard these unsaved changes and archive this maintenance window?', + } +); + export const EXPERIMENTAL_LABEL = i18n.translate( 'xpack.alerting.maintenanceWindows.badge.experimentalLabel', { diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.test.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.test.ts new file mode 100644 index 0000000000000..8f0e44eaf2eb9 --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { archiveMaintenanceWindow } from './archive'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('archiveMaintenanceWindow', () => { + test('should call archive maintenance window api', async () => { + const apiResponse = { + title: 'test', + duration: 1, + r_rule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + http.post.mockResolvedValueOnce(apiResponse); + + const maintenanceWindow: MaintenanceWindow = { + title: 'test', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + + const result = await archiveMaintenanceWindow({ + http, + maintenanceWindowId: '123', + archive: true, + }); + expect(result).toEqual(maintenanceWindow); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/maintenance_window/123/_archive", + Object { + "body": "{\\"archive\\":true}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts new file mode 100644 index 0000000000000..fe07ebb04b38e --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts @@ -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 { HttpSetup } from '@kbn/core/public'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; + +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; + +const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ + ...rest, + rRule, +}); + +export async function archiveMaintenanceWindow({ + http, + maintenanceWindowId, + archive, +}: { + http: HttpSetup; + maintenanceWindowId: string; + archive: boolean; +}): Promise { + const res = await http.post>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/${encodeURIComponent( + maintenanceWindowId + )}/_archive`, + { body: JSON.stringify({ archive }) } + ); + + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.test.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.test.ts new file mode 100644 index 0000000000000..a67b7246a64f5 --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '@kbn/core/public/mocks'; +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { finishMaintenanceWindow } from './finish'; + +const http = httpServiceMock.createStartContract(); + +beforeEach(() => jest.resetAllMocks()); + +describe('finishMaintenanceWindow', () => { + test('should call finish maintenance window api', async () => { + const apiResponse = { + title: 'test', + duration: 1, + r_rule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + http.post.mockResolvedValueOnce(apiResponse); + + const maintenanceWindow: MaintenanceWindow = { + title: 'test', + duration: 1, + rRule: { + dtstart: '2023-03-23T19:16:21.293Z', + tzid: 'America/New_York', + freq: 3, + interval: 1, + byweekday: ['TH'], + }, + }; + + const result = await finishMaintenanceWindow({ + http, + maintenanceWindowId: '123', + }); + expect(result).toEqual(maintenanceWindow); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/maintenance_window/123/_finish", + ] + `); + }); +}); diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts new file mode 100644 index 0000000000000..910fad0bee1c3 --- /dev/null +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from '@kbn/core/public'; +import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; + +import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; + +const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ + ...rest, + rRule, +}); + +export async function finishMaintenanceWindow({ + http, + maintenanceWindowId, +}: { + http: HttpSetup; + maintenanceWindowId: string; +}): Promise { + const res = await http.post>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/maintenance_window/${encodeURIComponent( + maintenanceWindowId + )}/_finish` + ); + + return rewriteBodyRes(res); +} diff --git a/x-pack/plugins/alerting/server/alert/alert.ts b/x-pack/plugins/alerting/server/alert/alert.ts index 1fff89d527d5a..36877db2726c6 100644 --- a/x-pack/plugins/alerting/server/alert/alert.ts +++ b/x-pack/plugins/alerting/server/alert/alert.ts @@ -7,7 +7,7 @@ import { v4 as uuidV4 } from 'uuid'; import { get, isEmpty } from 'lodash'; -import { ALERT_INSTANCE_ID } from '@kbn/rule-data-utils'; +import { ALERT_INSTANCE_ID, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; import { CombinedSummarizedAlerts } from '../types'; import { AlertInstanceMeta, @@ -277,7 +277,9 @@ export class Alert< } return !summarizedAlerts.all.data.some( - (alert) => get(alert, ALERT_INSTANCE_ID) === this.getId() + (alert) => + get(alert, ALERT_INSTANCE_ID) === this.getId() || + get(alert, ALERT_RULE_UUID) === this.getId() ); } } diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts index dbcb75a7816e7..45f8d84fd4a98 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.test.ts @@ -42,6 +42,7 @@ describe('alertSummaryFromEventLog', () => { "lastRun": undefined, "muteAll": false, "name": "rule-name", + "revision": 0, "ruleTypeId": "123", "status": "OK", "statusEndDate": "2020-06-18T01:00:00.000Z", @@ -88,6 +89,7 @@ describe('alertSummaryFromEventLog', () => { "lastRun": undefined, "muteAll": true, "name": "rule-name-2", + "revision": 0, "ruleTypeId": "456", "status": "OK", "statusEndDate": "2020-06-18T03:00:00.000Z", diff --git a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts index 7c3df21a3281d..d316b1b5bf01c 100644 --- a/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts +++ b/x-pack/plugins/alerting/server/lib/alert_summary_from_event_log.ts @@ -40,6 +40,7 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) average: 0, valuesWithTimestamp: {}, }, + revision: rule.revision, }; const alerts = new Map(); @@ -86,6 +87,10 @@ export function alertSummaryFromEventLog(params: AlertSummaryFromEventLogParams) status.flapping = true; } + if (event?.kibana?.alert?.maintenance_window_ids?.length) { + status.maintenanceWindowIds = event.kibana.alert.maintenance_window_ids as string[]; + } + switch (action) { case EVENT_LOG_ACTIONS.newInstance: status.activeStartDate = timeStamp; diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts index ff62ddaea7ff4..00a9bf7221ef3 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.mock.ts @@ -15,6 +15,7 @@ const createAlertingEventLoggerMock = () => { setRuleName: jest.fn(), setExecutionSucceeded: jest.fn(), setExecutionFailed: jest.fn(), + setMaintenanceWindowIds: jest.fn(), logTimeout: jest.fn(), logAlert: jest.fn(), logAction: jest.fn(), diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 2711f921e81ec..2b6075cd49f49 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -55,6 +55,7 @@ const context: RuleContextOpts = { spaceId: 'test-space', executionId: 'abcd-efgh-ijklmnop', taskScheduledAt: new Date('2020-01-01T00:00:00.000Z'), + ruleRevision: 0, }; const contextWithScheduleDelay = { ...context, taskScheduleDelay: 7200000 }; @@ -284,6 +285,51 @@ describe('AlertingEventLogger', () => { }); }); + describe('setMaintenanceWindowIds()', () => { + test('should throw error if alertingEventLogger has not been initialized', () => { + expect(() => + alertingEventLogger.setMaintenanceWindowIds([]) + ).toThrowErrorMatchingInlineSnapshot(`"AlertingEventLogger not initialized"`); + }); + + test('should throw error if event is null', () => { + alertingEventLogger.initialize(context); + expect(() => + alertingEventLogger.setMaintenanceWindowIds([]) + ).toThrowErrorMatchingInlineSnapshot(`"AlertingEventLogger not initialized"`); + }); + + it('should update event maintenance window IDs correctly', () => { + alertingEventLogger.initialize(context); + alertingEventLogger.start(); + alertingEventLogger.setMaintenanceWindowIds([]); + + const event = initializeExecuteRecord(contextWithScheduleDelay); + expect(alertingEventLogger.getEvent()).toEqual({ + ...event, + kibana: { + ...event.kibana, + alert: { + ...event.kibana?.alert, + maintenance_window_ids: [], + }, + }, + }); + + alertingEventLogger.setMaintenanceWindowIds(['test-id-1', 'test-id-2']); + expect(alertingEventLogger.getEvent()).toEqual({ + ...event, + kibana: { + ...event.kibana, + alert: { + ...event.kibana?.alert, + maintenance_window_ids: ['test-id-1', 'test-id-2'], + }, + }, + }); + }); + }); + describe('logTimeout()', () => { test('should throw error if alertingEventLogger has not been initialized', () => { expect(() => alertingEventLogger.logTimeout()).toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts index 37029b4c96703..3af760488a259 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.ts @@ -30,6 +30,7 @@ export interface RuleContextOpts { executionId: string; taskScheduledAt: Date; ruleName?: string; + ruleRevision?: number; } type RuleContext = RuleContextOpts & { @@ -138,6 +139,14 @@ export class AlertingEventLogger { updateEvent(this.event, { message, outcome: 'success', alertingOutcome: 'success' }); } + public setMaintenanceWindowIds(maintenanceWindowIds: string[]) { + if (!this.isInitialized || !this.event) { + throw new Error('AlertingEventLogger not initialized'); + } + + updateEvent(this.event, { maintenanceWindowIds }); + } + public setExecutionFailed(message: string, errorMessage: string) { if (!this.isInitialized || !this.event) { throw new Error('AlertingEventLogger not initialized'); @@ -258,6 +267,7 @@ export function createAlertRecord(context: RuleContextOpts, alert: AlertOpts) { ruleName: context.ruleName, flapping: alert.flapping, maintenanceWindowIds: alert.maintenanceWindowIds, + ruleRevision: context.ruleRevision, }); } @@ -288,6 +298,7 @@ export function createActionExecuteRecord(context: RuleContextOpts, action: Acti ], ruleName: context.ruleName, alertSummary: action.alertSummary, + ruleRevision: context.ruleRevision, }); } @@ -314,6 +325,7 @@ export function createExecuteTimeoutRecord(context: RuleContextOpts) { }, ], ruleName: context.ruleName, + ruleRevision: context.ruleRevision, }); } @@ -326,6 +338,7 @@ export function initializeExecuteRecord(context: RuleContext) { spaceId: context.spaceId, executionId: context.executionId, action: EVENT_LOG_ACTIONS.execute, + ruleRevision: context.ruleRevision, task: { scheduled: context.taskScheduledAt.toISOString(), scheduleDelay: Millis2Nanos * context.taskScheduleDelay, @@ -351,11 +364,22 @@ interface UpdateEventOpts { reason?: string; metrics?: RuleRunMetrics; timings?: TaskRunnerTimings; + maintenanceWindowIds?: string[]; } export function updateEvent(event: IEvent, opts: UpdateEventOpts) { - const { message, outcome, error, ruleName, status, reason, metrics, timings, alertingOutcome } = - opts; + const { + message, + outcome, + error, + ruleName, + status, + reason, + metrics, + timings, + alertingOutcome, + maintenanceWindowIds, + } = opts; if (!event) { throw new Error('Cannot update event because it is not initialized.'); } @@ -431,4 +455,10 @@ export function updateEvent(event: IEvent, opts: UpdateEventOpts) { ...timings, }; } + + if (maintenanceWindowIds) { + event.kibana = event.kibana || {}; + event.kibana.alert = event.kibana.alert || {}; + event.kibana.alert.maintenance_window_ids = maintenanceWindowIds; + } } diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts index e62ec213cba0f..6b18d1aac93dd 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts @@ -36,6 +36,7 @@ describe('createAlertEventLogRecordObject', () => { ruleType, consumer: 'rule-consumer', action: 'execute-start', + ruleRevision: 0, timestamp: '1970-01-01T00:00:00.000Z', task: { scheduled: '1970-01-01T00:00:00.000Z', @@ -66,6 +67,7 @@ describe('createAlertEventLogRecordObject', () => { execution: { uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', }, + revision: 0, rule_type_id: 'test', }, maintenance_window_ids: MAINTENANCE_WINDOW_IDS, @@ -107,6 +109,7 @@ describe('createAlertEventLogRecordObject', () => { group: 'group 1', message: 'message text here', namespace: 'default', + ruleRevision: 0, state: { start: '1970-01-01T00:00:00.000Z', end: '1970-01-01T00:05:00.000Z', @@ -139,6 +142,7 @@ describe('createAlertEventLogRecordObject', () => { execution: { uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', }, + revision: 0, rule_type_id: 'test', }, maintenance_window_ids: MAINTENANCE_WINDOW_IDS, @@ -182,6 +186,7 @@ describe('createAlertEventLogRecordObject', () => { group: 'group 1', message: 'action execution start', namespace: 'default', + ruleRevision: 0, state: { start: '1970-01-01T00:00:00.000Z', end: '1970-01-01T00:05:00.000Z', @@ -224,6 +229,7 @@ describe('createAlertEventLogRecordObject', () => { execution: { uuid: '7a7065d7-6e8b-4aae-8d20-c93613dec9fb', }, + revision: 0, rule_type_id: 'test', }, maintenance_window_ids: MAINTENANCE_WINDOW_IDS, diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts index 97284f0ce9f78..251df68e5267f 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.ts @@ -43,6 +43,7 @@ interface CreateAlertEventLogRecordParams { recovered: number; }; maintenanceWindowIds?: string[]; + ruleRevision?: number; } export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecordParams): Event { @@ -62,6 +63,7 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor alertUuid, alertSummary, maintenanceWindowIds, + ruleRevision, } = params; const alerting = params.instanceId || group || alertSummary @@ -97,6 +99,7 @@ export function createAlertEventLogRecordObject(params: CreateAlertEventLogRecor ...(maintenanceWindowIds ? { maintenance_window_ids: maintenanceWindowIds } : {}), ...(alertUuid ? { uuid: alertUuid } : {}), rule: { + revision: ruleRevision, rule_type_id: ruleType.id, ...(consumer ? { consumer } : {}), ...(executionId diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts index 6a57baaacef27..bb38fb7a98bfa 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.test.ts @@ -270,7 +270,7 @@ describe('getExecutionLogAggregation', () => { }, }, executionDuration: { max: { field: 'event.duration' } }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -283,6 +283,7 @@ describe('getExecutionLogAggregation', () => { 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', + 'kibana.alert.maintenance_window_ids', ], }, }, @@ -477,7 +478,7 @@ describe('getExecutionLogAggregation', () => { }, }, executionDuration: { max: { field: 'event.duration' } }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -490,6 +491,7 @@ describe('getExecutionLogAggregation', () => { 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', + 'kibana.alert.maintenance_window_ids', ], }, }, @@ -684,7 +686,7 @@ describe('getExecutionLogAggregation', () => { }, }, executionDuration: { max: { field: 'event.duration' } }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -697,6 +699,7 @@ describe('getExecutionLogAggregation', () => { 'kibana.space_ids', 'rule.name', 'kibana.alerting.outcome', + 'kibana.alert.maintenance_window_ids', ], }, }, @@ -774,7 +777,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -861,7 +864,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -880,6 +883,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -958,6 +964,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -981,6 +988,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -1022,7 +1030,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1112,7 +1120,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1131,6 +1139,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -1209,6 +1220,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1232,6 +1244,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -1273,7 +1286,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1355,7 +1368,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1374,6 +1387,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -1452,6 +1468,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -1475,6 +1492,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -1516,7 +1534,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1603,7 +1621,7 @@ describe('formatExecutionLogResult', () => { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -1622,6 +1640,9 @@ describe('formatExecutionLogResult', () => { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -1700,6 +1721,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: [], }, { id: '61bb867b-661a-471f-bf92-23471afa10b3', @@ -1723,6 +1745,7 @@ describe('formatExecutionLogResult', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); diff --git a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts index b65499de20d45..44e1f7bbe98c1 100644 --- a/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts +++ b/x-pack/plugins/alerting/server/lib/get_execution_log_aggregation.ts @@ -41,6 +41,7 @@ const NUMBER_OF_NEW_ALERTS_FIELD = 'kibana.alert.rule.execution.metrics.alert_co const NUMBER_OF_RECOVERED_ALERTS_FIELD = 'kibana.alert.rule.execution.metrics.alert_counts.recovered'; const EXECUTION_UUID_FIELD = 'kibana.alert.rule.execution.uuid'; +const MAINTENANCE_WINDOW_IDS_FIELD = 'kibana.alert.maintenance_window_ids'; const Millis2Nanos = 1000 * 1000; @@ -82,7 +83,8 @@ interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketK numActiveAlerts: estypes.AggregationsMaxAggregate; numRecoveredAlerts: estypes.AggregationsMaxAggregate; numNewAlerts: estypes.AggregationsMaxAggregate; - outcomeAndMessage: estypes.AggregationsTopHitsAggregate; + outcomeMessageAndMaintenanceWindow: estypes.AggregationsTopHitsAggregate; + maintenanceWindowIds: estypes.AggregationsTopHitsAggregate; }; actionExecution: { actionOutcomes: IActionExecution; @@ -401,7 +403,7 @@ export function getExecutionLogAggregation({ field: DURATION_FIELD, }, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { top_hits: { size: 1, _source: { @@ -414,6 +416,7 @@ export function getExecutionLogAggregation({ SPACE_ID_FIELD, RULE_NAME_FIELD, ALERTING_OUTCOME_FIELD, + MAINTENANCE_WINDOW_IDS_FIELD, ], }, }, @@ -485,20 +488,30 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio const actionExecutionError = actionExecutionOutcomes.find((subBucket) => subBucket?.key === 'failure')?.doc_count ?? 0; - const outcomeAndMessage = bucket?.ruleExecution?.outcomeAndMessage?.hits?.hits[0]?._source ?? {}; - let status = outcomeAndMessage.kibana?.alerting?.outcome ?? ''; + const outcomeMessageAndMaintenanceWindow = + bucket?.ruleExecution?.outcomeMessageAndMaintenanceWindow?.hits?.hits[0]?._source ?? {}; + let status = outcomeMessageAndMaintenanceWindow.kibana?.alerting?.outcome ?? ''; if (isEmpty(status)) { - status = outcomeAndMessage.event?.outcome ?? ''; + status = outcomeMessageAndMaintenanceWindow.event?.outcome ?? ''; } - const outcomeMessage = outcomeAndMessage.message ?? ''; - const outcomeErrorMessage = outcomeAndMessage.error?.message ?? ''; + const outcomeMessage = outcomeMessageAndMaintenanceWindow.message ?? ''; + const outcomeErrorMessage = outcomeMessageAndMaintenanceWindow.error?.message ?? ''; const message = status === 'failure' ? `${outcomeMessage} - ${outcomeErrorMessage}` : outcomeMessage; - const version = outcomeAndMessage.kibana?.version ?? ''; - - const ruleId = outcomeAndMessage ? outcomeAndMessage?.rule?.id ?? '' : ''; - const spaceIds = outcomeAndMessage ? outcomeAndMessage?.kibana?.space_ids ?? [] : []; - const ruleName = outcomeAndMessage ? outcomeAndMessage.rule?.name ?? '' : ''; + const version = outcomeMessageAndMaintenanceWindow.kibana?.version ?? ''; + + const ruleId = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow?.rule?.id ?? '' + : ''; + const spaceIds = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow?.kibana?.space_ids ?? [] + : []; + const maintenanceWindowIds = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow.kibana?.alert?.maintenance_window_ids ?? [] + : []; + const ruleName = outcomeMessageAndMaintenanceWindow + ? outcomeMessageAndMaintenanceWindow.rule?.name ?? '' + : ''; return { id: bucket?.key ?? '', timestamp: bucket?.ruleExecution?.executeStartTime.value_as_string ?? '', @@ -520,6 +533,7 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio rule_id: ruleId, space_ids: spaceIds, rule_name: ruleName, + maintenance_window_ids: maintenanceWindowIds, }; } diff --git a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts index fa30b0ff8d2ed..f22d7d2055b09 100644 --- a/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts +++ b/x-pack/plugins/alerting/server/routes/bulk_edit_rules.ts @@ -19,6 +19,17 @@ const ruleActionSchema = schema.object({ id: schema.string(), params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }), uuid: schema.maybe(schema.string()), + frequency: schema.maybe( + schema.object({ + summary: schema.boolean(), + throttle: schema.nullable(schema.string()), + notifyWhen: schema.oneOf([ + schema.literal('onActionGroupChange'), + schema.literal('onActiveAlert'), + schema.literal('onThrottleInterval'), + ]), + }) + ), }); const operationsSchema = schema.arrayOf( diff --git a/x-pack/plugins/alerting/server/routes/create_rule.test.ts b/x-pack/plugins/alerting/server/routes/create_rule.test.ts index 4285c5a986b2b..edd1d10d06236 100644 --- a/x-pack/plugins/alerting/server/routes/create_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/create_rule.test.ts @@ -56,6 +56,7 @@ describe('createRuleRoute', () => { query: { kql: 'name:test', dsl: '{"must": {"term": { "name": "test" }}}', + filters: [], }, timeframe: { days: [1], @@ -92,7 +93,7 @@ describe('createRuleRoute', () => { id: mockedAlert.actions[0].id, params: mockedAlert.actions[0].params, alerts_filter: { - query: { kql: mockedAlert.actions[0].alertsFilter!.query!.kql }, + query: { kql: mockedAlert.actions[0].alertsFilter!.query!.kql, filters: [] }, timeframe: mockedAlert.actions[0].alertsFilter?.timeframe!, }, }, @@ -167,6 +168,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { @@ -263,6 +265,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { @@ -360,6 +363,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { @@ -457,6 +461,7 @@ describe('createRuleRoute', () => { Object { "alertsFilter": Object { "query": Object { + "filters": Array [], "kql": "name:test", }, "timeframe": Object { diff --git a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts index 3ee2b0d1816ba..f6e1c3417a42f 100644 --- a/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_global_execution_logs.test.ts @@ -48,6 +48,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: ['namespace'], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -71,6 +72,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: ['namespace'], + maintenance_window_ids: ['test-id-1'], }, ], }; diff --git a/x-pack/plugins/alerting/server/routes/get_rule.test.ts b/x-pack/plugins/alerting/server/routes/get_rule.test.ts index 17481e538ed86..a672de9cbf320 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.test.ts @@ -49,6 +49,7 @@ describe('getRuleRoute', () => { query: { kql: 'name:test', dsl: '{"must": {"term": { "name": "test" }}}', + filters: [], }, timeframe: { days: [1], diff --git a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts index a6b0e88bbf5af..f7fe1a3406e9c 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_alert_summary.test.ts @@ -38,6 +38,7 @@ describe('getRuleAlertSummaryRoute', () => { status: 'OK', errorMessages: [], alerts: {}, + revision: 0, executionDuration: { average: 1, valuesWithTimestamp: { diff --git a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts index eb22a6429809a..a0dd57da558eb 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule_execution_log.test.ts @@ -49,6 +49,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: ['namespace'], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -72,6 +73,7 @@ describe('getRuleExecutionLogRoute', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule_name', space_ids: ['namespace'], + maintenance_window_ids: ['test-id-1'], }, ], }; diff --git a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts index 672ccee369aac..a78fcd7f86f65 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/get_alert_instance_summary.test.ts @@ -47,6 +47,7 @@ describe('getAlertInstanceSummaryRoute', () => { average: 0, valuesWithTimestamp: {}, }, + revision: 0, }; it('gets alert instance summary', async () => { diff --git a/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts b/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts index 3e561168aee48..9d6f89e070c3a 100644 --- a/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts +++ b/x-pack/plugins/alerting/server/routes/lib/actions_schema.ts @@ -29,13 +29,20 @@ export const actionsSchema = schema.arrayOf( uuid: schema.maybe(schema.string()), alerts_filter: schema.maybe( schema.object({ - query: schema.nullable( + query: schema.maybe( schema.object({ kql: schema.string(), + filters: schema.arrayOf( + schema.object({ + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), + meta: schema.recordOf(schema.string(), schema.any()), + state$: schema.maybe(schema.object({ store: schema.string() })), + }) + ), dsl: schema.maybe(schema.string()), }) ), - timeframe: schema.nullable( + timeframe: schema.maybe( schema.object({ days: schema.arrayOf( schema.oneOf([ diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts index b79e5a91ff381..61dc9282bbfa1 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_actions.test.ts @@ -27,6 +27,7 @@ describe('rewrite Actions', () => { query: { kql: 'test:1s', dsl: '{test:1}', + filters: [], }, timeframe: { days: [1, 2, 3], @@ -42,7 +43,7 @@ describe('rewrite Actions', () => { ).toEqual([ { alerts_filter: { - query: { dsl: '{test:1}', kql: 'test:1s' }, + query: { dsl: '{test:1}', kql: 'test:1s', filters: [] }, timeframe: { days: [1, 2, 3], hours: { end: '15:00', start: '00:00' }, @@ -77,6 +78,7 @@ describe('rewrite Actions', () => { query: { kql: 'test:1s', dsl: '{test:1}', + filters: [], }, timeframe: { days: [1, 2, 3], @@ -104,6 +106,7 @@ describe('rewrite Actions', () => { query: { kql: 'test:1s', dsl: '{test:1}', + filters: [], }, timeframe: { days: [1, 2, 3], diff --git a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts index d5d0ba1739355..7a348e583ac6c 100644 --- a/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/lib/rewrite_rule.test.ts @@ -43,7 +43,7 @@ const sampleRule: SanitizedRule & { activeSnoozes?: string[] } = notifyWhen: 'onThrottleInterval', throttle: '1m', }, - alertsFilter: { timeframe: null, query: { kql: 'test:1', dsl: '{}' } }, + alertsFilter: { query: { kql: 'test:1', dsl: '{}', filters: [] } }, }, ], scheduledTaskId: 'xyz456', diff --git a/x-pack/plugins/alerting/server/routes/update_rule.test.ts b/x-pack/plugins/alerting/server/routes/update_rule.test.ts index 4dfe1c00145e8..5a4b3a19c0d7c 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.test.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.test.ts @@ -53,8 +53,8 @@ describe('updateRuleRoute', () => { query: { kql: 'name:test', dsl: '{"must": {"term": { "name": "test" }}}', + filters: [], }, - timeframe: null, }, }, ], @@ -123,9 +123,9 @@ describe('updateRuleRoute', () => { "alertsFilter": Object { "query": Object { "dsl": "{\\"must\\": {\\"term\\": { \\"name\\": \\"test\\" }}}", + "filters": Array [], "kql": "name:test", }, - "timeframe": null, }, "group": "default", "id": "2", diff --git a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts index 7ee9d323c7627..6605d9c22a72b 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.test.ts @@ -24,7 +24,15 @@ describe('addGeneratedActionValues()', () => { throttle: null, }, alertsFilter: { - query: { kql: 'test:testValue' }, + query: { + kql: 'test:testValue', + filters: [ + { + meta: { key: 'foo', params: { query: 'bar' } }, + query: { match_phrase: { foo: 'bar ' } }, + }, + ], + }, timeframe: { days: [1, 2], hours: { start: '08:00', end: '17:00' }, @@ -41,14 +49,17 @@ describe('addGeneratedActionValues()', () => { test('adds DSL', async () => { const actionWithGeneratedValues = addGeneratedActionValues([mockAction]); expect(actionWithGeneratedValues[0].alertsFilter?.query?.dsl).toBe( - '{"bool":{"should":[{"match":{"test":"testValue"}}],"minimum_should_match":1}}' + '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"test":"testValue"}}],"minimum_should_match":1}},{"match_phrase":{"foo":"bar "}}],"should":[],"must_not":[]}}' ); }); test('throws error if KQL is not valid', async () => { expect(() => addGeneratedActionValues([ - { ...mockAction, alertsFilter: { query: { kql: 'foo:bar:1' }, timeframe: null } }, + { + ...mockAction, + alertsFilter: { query: { kql: 'foo:bar:1', filters: [] } }, + }, ]) ).toThrowErrorMatchingInlineSnapshot('"Error creating DSL query: invalid KQL"'); }); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts index c3cd721698594..71cbf01ea1a4e 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/add_generated_action_values.ts @@ -6,7 +6,7 @@ */ import { v4 } from 'uuid'; -import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +import { buildEsQuery, Filter } from '@kbn/es-query'; import Boom from '@hapi/boom'; import { NormalizedAlertAction, NormalizedAlertActionWithGeneratedValues } from '..'; @@ -14,9 +14,11 @@ export function addGeneratedActionValues( actions: NormalizedAlertAction[] = [] ): NormalizedAlertActionWithGeneratedValues[] { return actions.map(({ uuid, alertsFilter, ...action }) => { - const generateDSL = (kql: string) => { + const generateDSL = (kql: string, filters: Filter[]) => { try { - return JSON.stringify(toElasticsearchQuery(fromKueryExpression(kql))); + return JSON.stringify( + buildEsQuery(undefined, [{ query: kql, language: 'kuery' }], filters) + ); } catch (e) { throw Boom.badRequest(`Error creating DSL query: invalid KQL`); } @@ -29,13 +31,12 @@ export function addGeneratedActionValues( ? { alertsFilter: { ...alertsFilter, - timeframe: alertsFilter.timeframe || null, - query: !alertsFilter.query - ? null - : { - kql: alertsFilter.query.kql, - dsl: generateDSL(alertsFilter.query.kql), - }, + query: alertsFilter.query + ? { + ...alertsFilter.query, + dsl: generateDSL(alertsFilter.query.kql, alertsFilter.query.filters), + } + : undefined, }, } : {}), diff --git a/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts index bf8d10a341597..9648f23dc05d2 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/recover_rule_alerts.ts @@ -43,6 +43,7 @@ export const recoverRuleAlerts = async ( const event = createAlertEventLogRecordObject({ ruleId: id, ruleName: attributes.name, + ruleRevision: attributes.revision, ruleType: context.ruleTypeRegistry.get(attributes.alertTypeId), consumer: attributes.consumer, instanceId: alertId, diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts index 8689113ca7907..a0aa3286f1f6d 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/format_legacy_actions.ts @@ -14,6 +14,7 @@ import { injectReferencesIntoActions } from '../../common'; import { transformToNotifyWhen } from './transform_to_notify_when'; import { transformFromLegacyActions } from './transform_legacy_actions'; import { LegacyIRuleActionsAttributes, legacyRuleActionsSavedObjectType } from './types'; +import { transformToAlertThrottle } from './transform_to_alert_throttle'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -136,7 +137,9 @@ export const formatLegacyActions = async ( return { ...rule, actions: [...rule.actions, ...legacyRuleActions], - throttle: (legacyRuleActions.length ? ruleThrottle : rule.throttle) ?? 'no_actions', + throttle: transformToAlertThrottle( + (legacyRuleActions.length ? ruleThrottle : rule.throttle) ?? 'no_actions' + ), notifyWhen: transformToNotifyWhen(ruleThrottle), // muteAll property is disregarded in further rule processing in Security Solution when legacy actions are present. // So it should be safe to set it as false, so it won't be displayed to user as w/o actions see transformFromAlertThrottle method diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts index b23798294b300..cea32ce3e1913 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.test.ts @@ -67,7 +67,7 @@ describe('transformFromLegacyActions', () => { (transformToNotifyWhen as jest.Mock).mockReturnValueOnce(null); const actions = transformFromLegacyActions(legacyActionsAttr, references); - expect(actions[0].frequency?.notifyWhen).toBe('onThrottleInterval'); + expect(actions[0].frequency?.notifyWhen).toBe('onActiveAlert'); }); it('should return transformed legacy actions', () => { diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts index 485a32f781695..1218e96a8dfd7 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_legacy_actions.ts @@ -11,6 +11,7 @@ import type { SavedObjectReference } from '@kbn/core/server'; import { RawRuleAction } from '../../../types'; import { transformToNotifyWhen } from './transform_to_notify_when'; import { LegacyIRuleActionsAttributes } from './types'; +import { transformToAlertThrottle } from './transform_to_alert_throttle'; /** * @deprecated Once we are confident all rules relying on side-car actions SO's have been migrated to SO references we should remove this function @@ -50,8 +51,8 @@ export const transformFromLegacyActions = ( actionTypeId, frequency: { summary: true, - notifyWhen: transformToNotifyWhen(legacyActionsAttr.ruleThrottle) ?? 'onThrottleInterval', - throttle: legacyActionsAttr.ruleThrottle, + notifyWhen: transformToNotifyWhen(legacyActionsAttr.ruleThrottle) ?? 'onActiveAlert', + throttle: transformToAlertThrottle(legacyActionsAttr.ruleThrottle), }, }, ]; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts new file mode 100644 index 0000000000000..f52742009503a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformToAlertThrottle } from './transform_to_alert_throttle'; + +describe('transformToAlertThrottle', () => { + it('should return null when throttle is null OR no_actions', () => { + expect(transformToAlertThrottle(null)).toBeNull(); + expect(transformToAlertThrottle('rule')).toBeNull(); + expect(transformToAlertThrottle('no_actions')).toBeNull(); + }); + it('should return same value for other throttle values', () => { + expect(transformToAlertThrottle('1h')).toBe('1h'); + expect(transformToAlertThrottle('1m')).toBe('1m'); + expect(transformToAlertThrottle('1d')).toBe('1d'); + }); +}); diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts new file mode 100644 index 0000000000000..7f6ecee900e2f --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/transform_to_alert_throttle.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Given a throttle from a "security_solution" rule this will transform it into an "alerting" "throttle" + * on their saved object. + * @params throttle The throttle from a "security_solution" rule + * @returns The "alerting" throttle + */ +export const transformToAlertThrottle = (throttle: string | null | undefined): string | null => { + if (throttle == null || throttle === 'rule' || throttle === 'no_actions') { + return null; + } else { + return throttle; + } +}; diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts index 679f79d477c86..229c009df3eec 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts @@ -45,7 +45,7 @@ describe('validateActions', () => { throttle: null, }, alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, timeframe: { days: [1], hours: { start: '10:00', end: '17:00' }, timezone: 'UTC' }, }, }, @@ -200,7 +200,7 @@ describe('validateActions', () => { { ...data.actions[0], alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, timeframe: { days: [1], hours: { start: '30:00', end: '17:00' }, timezone: 'UTC' }, }, }, @@ -237,7 +237,7 @@ describe('validateActions', () => { { ...data.actions[0], alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, // @ts-ignore timeframe: { days: [1], hours: { start: '10:00', end: '17:00' } }, }, @@ -261,7 +261,7 @@ describe('validateActions', () => { { ...data.actions[0], alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, timeframe: { // @ts-ignore days: [0, 8], diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts index 357f624a815cd..b86e30556f090 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.ts @@ -154,7 +154,6 @@ export async function validateActions( ) { actionWithInvalidTimeframe.push(action); } - // alertsFilter time range filter's start time can't be before end time if (alertsFilter.timeframe.hours) { if ( validateHours(alertsFilter.timeframe.hours.start) || diff --git a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts index ce11a0cc017e3..640f3c3f324b3 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/bulk_edit.ts @@ -7,7 +7,7 @@ import pMap from 'p-map'; import Boom from '@hapi/boom'; -import { cloneDeep, omit } from 'lodash'; +import { cloneDeep } from 'lodash'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { KueryNode, nodeBuilder } from '@kbn/es-query'; import { @@ -638,19 +638,6 @@ async function getUpdatedAttributesFromOperations( isAttributesUpdateSkipped = false; } - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = updatedOperation.value.find( - (action) => action?.frequency - )?.frequency; - if (rule.attributes.consumer === AlertConsumers.SIEM && firstFrequency) { - ruleActions.actions = ruleActions.actions.map((action) => omit(action, 'frequency')); - if (!attributes.notifyWhen) { - attributes.notifyWhen = firstFrequency.notifyWhen; - attributes.throttle = firstFrequency.throttle; - } - } - break; } case 'snoozeSchedule': { diff --git a/x-pack/plugins/alerting/server/rules_client/methods/create.ts b/x-pack/plugins/alerting/server/rules_client/methods/create.ts index 90d6a4c49f90a..fd83a7b9b92e1 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/create.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/create.ts @@ -6,8 +6,6 @@ */ import Semver from 'semver'; import Boom from '@hapi/boom'; -import { omit } from 'lodash'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import { SavedObjectsUtils } from '@kbn/core/server'; import { withSpan } from '@kbn/apm-utils'; import { parseDuration } from '../../../common/parse_duration'; @@ -111,17 +109,6 @@ export async function create( throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`); } - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency; - if (data.consumer === AlertConsumers.SIEM && firstFrequency) { - data.actions = data.actions.map((action) => omit(action, 'frequency')); - if (!data.notifyWhen) { - data.notifyWhen = firstFrequency.notifyWhen; - data.throttle = firstFrequency.throttle; - } - } - await withSpan({ name: 'validateActions', type: 'rules' }, () => validateActions(context, ruleType, data, allowMissingConnectorSecrets) ); diff --git a/x-pack/plugins/alerting/server/rules_client/methods/update.ts b/x-pack/plugins/alerting/server/rules_client/methods/update.ts index 7c1fbedff37ce..f68b41067ddae 100644 --- a/x-pack/plugins/alerting/server/rules_client/methods/update.ts +++ b/x-pack/plugins/alerting/server/rules_client/methods/update.ts @@ -6,9 +6,8 @@ */ import Boom from '@hapi/boom'; -import { isEqual, omit } from 'lodash'; +import { isEqual } from 'lodash'; import { SavedObject } from '@kbn/core/server'; -import { AlertConsumers } from '@kbn/rule-data-utils'; import type { ShouldIncrementRevision } from './bulk_edit'; import { PartialRule, @@ -186,17 +185,6 @@ async function updateAlert( const ruleType = context.ruleTypeRegistry.get(attributes.alertTypeId); - // TODO https://github.com/elastic/kibana/issues/148414 - // If any action-level frequencies get pushed into a SIEM rule, strip their frequencies - const firstFrequency = data.actions.find((action) => action?.frequency)?.frequency; - if (attributes.consumer === AlertConsumers.SIEM && firstFrequency) { - data.actions = data.actions.map((action) => omit(action, 'frequency')); - if (!attributes.notifyWhen) { - attributes.notifyWhen = firstFrequency.notifyWhen; - attributes.throttle = firstFrequency.throttle; - } - } - // Validate const validatedAlertTypeParams = validateRuleTypeParams(data.params, ruleType.validate.params); await validateActions(context, ruleType, data, allowMissingConnectorSecrets); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts index 47b38cce094ba..3598ad31fd390 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/bulk_edit.test.ts @@ -706,7 +706,8 @@ describe('bulkEdit()', () => { alertsFilter: { query: { kql: 'name:test', - dsl: '{"bool":{"should":[{"match":{"name":"test"}}],"minimum_should_match":1}}', + dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"name":"test"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}', + filters: [], }, timeframe: { days: [1], @@ -725,7 +726,7 @@ describe('bulkEdit()', () => { id: '2', params: {}, uuid: '222', - alertsFilter: { query: { kql: 'test:1', dsl: 'test' } }, + alertsFilter: { query: { kql: 'test:1', dsl: 'test', filters: [] } }, }; unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({ @@ -744,8 +745,7 @@ describe('bulkEdit()', () => { actionRef: 'action_1', uuid: '222', alertsFilter: { - query: { kql: 'test:1', dsl: 'test' }, - timeframe: null, + query: { kql: 'test:1', dsl: 'test', filters: [] }, }, }, ], @@ -802,10 +802,10 @@ describe('bulkEdit()', () => { uuid: '222', alertsFilter: { query: { - dsl: '{"bool":{"should":[{"match":{"test":"1"}}],"minimum_should_match":1}}', + dsl: '{"bool":{"must":[],"filter":[{"bool":{"should":[{"match":{"test":"1"}}],"minimum_should_match":1}}],"should":[],"must_not":[]}}', kql: 'test:1', + filters: [], }, - timeframe: null, }, }, ], @@ -835,8 +835,8 @@ describe('bulkEdit()', () => { query: { dsl: 'test', kql: 'test:1', + filters: [], }, - timeframe: null, }, }, ], 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 d44d69a6e64bf..aedf130e1f846 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 @@ -3343,7 +3343,7 @@ describe('create()', () => { throttle: null, }, alertsFilter: { - query: { kql: 'test:1' }, + query: { kql: 'test:1', filters: [] }, }, }, ], 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 c09b491dbbdcc..b135f9b0ee23d 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 @@ -100,6 +100,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: true, + revision: 0, scheduledTaskId: '1', actions: [ { @@ -224,6 +225,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, + revision: 0, scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -277,6 +279,7 @@ describe('disable()', () => { }, params: { alertId: '1', + revision: 0, }, ownerId: null, }); @@ -296,6 +299,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, + revision: 0, scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -333,6 +337,7 @@ describe('disable()', () => { uuid: 'uuid-1', rule: { consumer: 'myApp', + revision: 0, rule_type_id: '123', }, }, @@ -379,6 +384,7 @@ describe('disable()', () => { meta: { versionApiKeyLastmodified: 'v7.10.0', }, + revision: 0, scheduledTaskId: '1', apiKey: 'MTIzOmFiYw==', apiKeyOwner: 'elastic', @@ -425,6 +431,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + revision: 0, scheduledTaskId: '1', updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -517,6 +524,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + revision: 0, scheduledTaskId: null, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', @@ -565,6 +573,7 @@ describe('disable()', () => { schedule: { interval: '10s' }, alertTypeId: 'myType', enabled: false, + revision: 0, scheduledTaskId: null, updatedAt: '2019-02-12T21:01:22.479Z', updatedBy: 'elastic', diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts index 212977a8381c2..dbfcc5f3fc017 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_alert_summary.test.ts @@ -189,6 +189,7 @@ describe('getAlertSummary()', () => { "lastRun": "2019-02-12T21:01:32.479Z", "muteAll": false, "name": "rule-name", + "revision": 0, "ruleTypeId": "123", "status": "Active", "statusEndDate": "2019-02-12T21:01:22.479Z", diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts index cebe9fa963971..33fb19c40f7fd 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_execution_log.test.ts @@ -133,7 +133,7 @@ const aggregateResults = { numRecoveredAlerts: { value: 0.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -242,7 +242,7 @@ const aggregateResults = { numRecoveredAlerts: { value: 5.0, }, - outcomeAndMessage: { + outcomeMessageAndMaintenanceWindow: { hits: { total: { value: 1, @@ -261,6 +261,9 @@ const aggregateResults = { }, kibana: { version: '8.2.0', + alert: { + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], + }, alerting: { outcome: 'success', }, @@ -389,6 +392,7 @@ describe('getExecutionLogForRule()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -412,6 +416,7 @@ describe('getExecutionLogForRule()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); @@ -725,6 +730,7 @@ describe('getGlobalExecutionLogWithAuth()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: [], }, { id: '41b2755e-765a-4044-9745-b03875d5e79a', @@ -748,6 +754,7 @@ describe('getGlobalExecutionLogWithAuth()', () => { rule_id: 'a348a740-9e2c-11ec-bd64-774ed95c43ef', rule_name: 'rule-name', space_ids: [], + maintenance_window_ids: ['254699b0-dfb2-11ed-bb3d-c91b918d0260'], }, ], }); diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts index 3aba014226042..03e1ba5976a62 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts @@ -947,7 +947,7 @@ describe('Execution Handler', () => { message: 'New: {{alerts.new.count}} Ongoing: {{alerts.ongoing.count}} Recovered: {{alerts.recovered.count}}', }, - alertsFilter: { query: { kql: 'test:1', dsl: '{}' } }, + alertsFilter: { query: { kql: 'test:1', dsl: '{}', filters: [] } }, }, ], }, @@ -1332,7 +1332,7 @@ describe('Execution Handler', () => { 'New: {{alerts.new.count}} Ongoing: {{alerts.ongoing.count}} Recovered: {{alerts.recovered.count}}', }, alertsFilter: { - query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}' }, + query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] }, }, }, ], @@ -1351,7 +1351,7 @@ describe('Execution Handler', () => { spaceId: 'test1', excludedAlertInstanceIds: ['foo'], alertsFilter: { - query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}' }, + query: { kql: 'kibana.alert.rule.name:foo', dsl: '{}', filters: [] }, }, }); expect(actionsClient.bulkEnqueueExecution).not.toHaveBeenCalled(); @@ -1392,7 +1392,7 @@ describe('Execution Handler', () => { }, params: {}, alertsFilter: { - query: { kql: 'kibana.alert.instance.id:1', dsl: '{}' }, + query: { kql: 'kibana.alert.instance.id:1', dsl: '{}', filters: [] }, }, }, ], @@ -1412,7 +1412,7 @@ describe('Execution Handler', () => { spaceId: 'test1', excludedAlertInstanceIds: ['foo'], alertsFilter: { - query: { kql: 'kibana.alert.instance.id:1', dsl: '{}' }, + query: { kql: 'kibana.alert.instance.id:1', dsl: '{}', filters: [] }, }, }); expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledWith([ diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 0b5cd235185d0..9be9064908fcb 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -409,6 +409,7 @@ export const mockAAD = { execution: { uuid: 'c35db7cc-5bf7-46ea-b43f-b251613a5b72' }, name: 'test-rule', producer: 'infrastructure', + revision: 0, rule_type_id: 'metrics.alert.threshold', uuid: '0de91960-7643-11ed-b719-bb9db8582cb6', tags: [], diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index ea37e5d8594d6..216cf19373cfa 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -678,11 +678,14 @@ describe('Task Runner', () => { 'Updating rule task for test rule with id 1 - {"lastExecutionDate":"1970-01-01T00:00:00.000Z","status":"active"} - {"outcome":"succeeded","outcomeOrder":0,"outcomeMsg":null,"warning":null,"alertsCount":{"active":1,"new":1,"recovered":0,"ignored":0}}' ); + const maintenanceWindowIds = ['test-id-1', 'test-id-2']; + testAlertingEventLogCalls({ activeAlerts: 1, newAlerts: 1, status: 'active', logAlert: 2, + maintenanceWindowIds, }); expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( 1, @@ -690,7 +693,7 @@ describe('Task Runner', () => { action: EVENT_LOG_ACTIONS.newInstance, group: 'default', state: { start: DATE_1970, duration: '0' }, - maintenanceWindowIds: ['test-id-1', 'test-id-2'], + maintenanceWindowIds, }) ); expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( @@ -699,7 +702,7 @@ describe('Task Runner', () => { action: EVENT_LOG_ACTIONS.activeInstance, group: 'default', state: { start: DATE_1970, duration: '0' }, - maintenanceWindowIds: ['test-id-1', 'test-id-2'], + maintenanceWindowIds, }) ); @@ -3113,6 +3116,7 @@ describe('Task Runner', () => { errorMessage = 'GENERIC ERROR MESSAGE', executionStatus = 'succeeded', setRuleName = true, + maintenanceWindowIds, logAlert = 0, logAction = 0, hasReachedAlertLimit = false, @@ -3126,6 +3130,7 @@ describe('Task Runner', () => { generatedActions?: number; executionStatus?: 'succeeded' | 'failed' | 'not-reached'; setRuleName?: boolean; + maintenanceWindowIds?: string[]; logAlert?: number; logAction?: number; errorReason?: string; @@ -3140,6 +3145,11 @@ describe('Task Runner', () => { expect(alertingEventLogger.setRuleName).not.toHaveBeenCalled(); } expect(alertingEventLogger.getStartAndDuration).toHaveBeenCalled(); + if (maintenanceWindowIds?.length) { + expect(alertingEventLogger.setMaintenanceWindowIds).toHaveBeenCalledWith( + maintenanceWindowIds + ); + } if (status === 'error') { expect(alertingEventLogger.done).toHaveBeenCalledWith({ metrics: null, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 41426724187e1..b4ee45eea902b 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -332,6 +332,9 @@ export class TaskRunner< const maintenanceWindowIds = activeMaintenanceWindows.map( (maintenanceWindow) => maintenanceWindow.id ); + if (maintenanceWindowIds.length) { + this.alertingEventLogger.setMaintenanceWindowIds(maintenanceWindowIds); + } const { updatedRuleTypeState } = await this.timer.runWithTimer( TaskRunnerTimerSpan.RuleTypeRun, diff --git a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts index cd581afafa266..a8889bc620c83 100644 --- a/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/transform_action_params.test.ts @@ -182,10 +182,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"1\\" exists", - } - `); + Object { + "message": "Value \\"1\\" exists", + } + `); }); test('alertName is passed to templates', () => { @@ -212,10 +212,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"alert-name\\" exists", - } - `); + Object { + "message": "Value \\"alert-name\\" exists", + } + `); }); test('tags is passed to templates', () => { @@ -242,10 +242,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"tag-A,tag-B\\" exists", - } - `); + Object { + "message": "Value \\"tag-A,tag-B\\" exists", + } + `); }); test('undefined tags is passed to templates', () => { @@ -271,10 +271,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"\\" is undefined and renders as empty string", - } - `); + Object { + "message": "Value \\"\\" is undefined and renders as empty string", + } + `); }); test('empty tags is passed to templates', () => { @@ -301,10 +301,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"\\" is an empty array and renders as empty string", - } - `); + Object { + "message": "Value \\"\\" is an empty array and renders as empty string", + } + `); }); test('spaceId is passed to templates', () => { @@ -331,10 +331,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"spaceId-A\\" exists", - } - `); + Object { + "message": "Value \\"spaceId-A\\" exists", + } + `); }); test('alertInstanceId is passed to templates', () => { @@ -361,10 +361,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"2\\" exists", - } - `); + Object { + "message": "Value \\"2\\" exists", + } + `); }); test('alertActionGroup is passed to templates', () => { @@ -391,10 +391,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"action-group\\" exists", - } - `); + Object { + "message": "Value \\"action-group\\" exists", + } + `); }); test('alertActionGroupName is passed to templates', () => { @@ -421,10 +421,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"Action Group\\" exists", - } - `); + Object { + "message": "Value \\"Action Group\\" exists", + } + `); }); test('rule variables are passed to templates', () => { @@ -451,10 +451,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"1\\", \\"alert-name\\", \\"spaceId-A\\" and \\"tag-A,tag-B\\" exist", - } - `); + Object { + "message": "Value \\"1\\", \\"alert-name\\", \\"spaceId-A\\" and \\"tag-A,tag-B\\" exist", + } + `); }); test('rule alert variables are passed to templates', () => { @@ -482,10 +482,10 @@ describe('transformActionParams', () => { flapping: false, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"2\\", \\"action-group\\", \\"uuid-1\\" and \\"Action Group\\" exist", - } - `); + Object { + "message": "Value \\"2\\", \\"action-group\\", \\"uuid-1\\" and \\"Action Group\\" exist", + } + `); }); test('date is passed to templates', () => { @@ -613,10 +613,10 @@ describe('transformActionParams', () => { flapping: true, }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"true\\" exists", - } - `); + Object { + "message": "Value \\"true\\" exists", + } + `); }); }); @@ -665,9 +665,9 @@ describe('transformSummaryActionParams', () => { const result = transformSummaryActionParams({ ...params, actionParams }); expect(result).toMatchInlineSnapshot(` - Object { - "message": "Value \\"{\\"@timestamp\\":\\"2022-12-07T15:38:43.472Z\\",\\"event\\":{\\"kind\\":\\"signal\\",\\"action\\":\\"active\\"},\\"kibana\\":{\\"version\\":\\"8.7.0\\",\\"space_ids\\":[\\"default\\"],\\"alert\\":{\\"instance\\":{\\"id\\":\\"*\\"},\\"uuid\\":\\"2d3e8fe5-3e8b-4361-916e-9eaab0bf2084\\",\\"status\\":\\"active\\",\\"workflow_status\\":\\"open\\",\\"reason\\":\\"system.cpu is 90% in the last 1 min for all hosts. Alert when > 50%.\\",\\"time_range\\":{\\"gte\\":\\"2022-01-01T12:00:00.000Z\\"},\\"start\\":\\"2022-12-07T15:23:13.488Z\\",\\"duration\\":{\\"us\\":100000},\\"flapping\\":false,\\"rule\\":{\\"category\\":\\"Metric threshold\\",\\"consumer\\":\\"alerts\\",\\"execution\\":{\\"uuid\\":\\"c35db7cc-5bf7-46ea-b43f-b251613a5b72\\"},\\"name\\":\\"test-rule\\",\\"producer\\":\\"infrastructure\\",\\"rule_type_id\\":\\"metrics.alert.threshold\\",\\"uuid\\":\\"0de91960-7643-11ed-b719-bb9db8582cb6\\",\\"tags\\":[]}}}}\\", \\"http://ruleurl\\" and \\"{\\"foo\\":\\"bar\\",\\"foo_bar\\":true,\\"name\\":\\"test-rule\\",\\"id\\":\\"1\\"}\\" exist", - } + Object { + "message": "Value \\"{\\"@timestamp\\":\\"2022-12-07T15:38:43.472Z\\",\\"event\\":{\\"kind\\":\\"signal\\",\\"action\\":\\"active\\"},\\"kibana\\":{\\"version\\":\\"8.7.0\\",\\"space_ids\\":[\\"default\\"],\\"alert\\":{\\"instance\\":{\\"id\\":\\"*\\"},\\"uuid\\":\\"2d3e8fe5-3e8b-4361-916e-9eaab0bf2084\\",\\"status\\":\\"active\\",\\"workflow_status\\":\\"open\\",\\"reason\\":\\"system.cpu is 90% in the last 1 min for all hosts. Alert when > 50%.\\",\\"time_range\\":{\\"gte\\":\\"2022-01-01T12:00:00.000Z\\"},\\"start\\":\\"2022-12-07T15:23:13.488Z\\",\\"duration\\":{\\"us\\":100000},\\"flapping\\":false,\\"rule\\":{\\"category\\":\\"Metric threshold\\",\\"consumer\\":\\"alerts\\",\\"execution\\":{\\"uuid\\":\\"c35db7cc-5bf7-46ea-b43f-b251613a5b72\\"},\\"name\\":\\"test-rule\\",\\"producer\\":\\"infrastructure\\",\\"revision\\":0,\\"rule_type_id\\":\\"metrics.alert.threshold\\",\\"uuid\\":\\"0de91960-7643-11ed-b719-bb9db8582cb6\\",\\"tags\\":[]}}}}\\", \\"http://ruleurl\\" and \\"{\\"foo\\":\\"bar\\",\\"foo_bar\\":true,\\"name\\":\\"test-rule\\",\\"id\\":\\"1\\"}\\" exist", + } `); }); diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 21ba86dc0f25f..420025b1cd007 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -23,6 +23,7 @@ import { import type { PublicMethodsOf } from '@kbn/utility-types'; import { SharePluginStart } from '@kbn/share-plugin/server'; import { type FieldMap } from '@kbn/alerts-as-data-utils'; +import { Filter } from '@kbn/es-query'; import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry'; import { PluginSetupContract, PluginStartContract } from './plugin'; import { RulesClient } from './rules_client'; @@ -285,11 +286,12 @@ export type UntypedRuleType = RuleType< >; export interface RawAlertsFilter extends AlertsFilter { - query: null | { + query?: { kql: string; + filters: Filter[]; dsl: string; }; - timeframe: null | AlertsFilterTimeframe; + timeframe?: AlertsFilterTimeframe; } export interface RawRuleAction extends SavedObjectAttributes { diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 21cecfcf348f7..7782cc044e950 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { AgentName } from '../typings/es_schemas/ui/fields/agent'; +import { + AgentName, + OpenTelemetryAgentName, +} from '../typings/es_schemas/ui/fields/agent'; import { ServerlessType } from './serverless'; /* @@ -47,8 +50,11 @@ export const AGENT_NAMES: AgentName[] = [ ...OPEN_TELEMETRY_AGENT_NAMES, ]; -export const isOpenTelemetryAgentName = (agentName: AgentName) => - OPEN_TELEMETRY_AGENT_NAMES.includes(agentName); +export function isOpenTelemetryAgentName( + agentName: string +): agentName is OpenTelemetryAgentName { + return OPEN_TELEMETRY_AGENT_NAMES.includes(agentName as AgentName); +} export const JAVA_AGENT_NAMES: AgentName[] = ['java', 'opentelemetry/java']; diff --git a/x-pack/plugins/apm/common/rules/schema.ts b/x-pack/plugins/apm/common/rules/schema.ts index 698b4507c5b3f..ca77e76f6f156 100644 --- a/x-pack/plugins/apm/common/rules/schema.ts +++ b/x-pack/plugins/apm/common/rules/schema.ts @@ -52,6 +52,7 @@ export const transactionErrorRateParamsSchema = schema.object({ windowUnit: schema.string(), threshold: schema.number(), transactionType: schema.maybe(schema.string()), + transactionName: schema.maybe(schema.string()), serviceName: schema.maybe(schema.string()), environment: schema.string(), }); diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.tsx new file mode 100644 index 0000000000000..cd94439db0389 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.stories.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 { Story } from '@storybook/react'; +import React, { ComponentType, useState } from 'react'; +import { CoreStart } from '@kbn/core/public'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { RuleParams, TransactionErrorRateRuleType } from '.'; +import { AlertMetadata } from '../../utils/helper'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; + +const KibanaReactContext = createKibanaReactContext({ + notifications: { toasts: { add: () => {} } }, +} as unknown as Partial); + +interface Args { + ruleParams: RuleParams; + metadata?: AlertMetadata; +} + +export default { + title: 'alerting/TransactionErrorRateRuleType', + component: TransactionErrorRateRuleType, + decorators: [ + (StoryComponent: ComponentType) => { + return ( + +
+ +
+
+ ); + }, + ], +}; + +export const CreatingInApmServiceOverview: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInApmServiceOverview.args = { + ruleParams: { + environment: 'testEnvironment', + serviceName: 'testServiceName', + threshold: 1500, + transactionType: 'testTransactionType', + transactionName: 'GET /api/customer/:id', + windowSize: 5, + windowUnit: 'm', + }, + metadata: { + environment: ENVIRONMENT_ALL.value, + serviceName: undefined, + }, +}; + +export const CreatingInStackManagement: Story = ({ + ruleParams, + metadata, +}) => { + const [params, setParams] = useState(ruleParams); + + function setRuleParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; + +CreatingInStackManagement.args = { + ruleParams: { + environment: 'testEnvironment', + threshold: 1500, + windowSize: 5, + windowUnit: 'm', + }, + metadata: undefined, +}; diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx index f9cfd6a511ef2..f161ef085b3ea 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/transaction_error_rate_rule_type/index.tsx @@ -23,20 +23,22 @@ import { IsAboveField, ServiceField, TransactionTypeField, + TransactionNameField, } from '../../utils/fields'; import { AlertMetadata, getIntervalAndTimeRange } from '../../utils/helper'; import { ApmRuleParamsContainer } from '../../ui_components/apm_rule_params_container'; -interface RuleParams { +export interface RuleParams { windowSize?: number; windowUnit?: string; threshold?: number; serviceName?: string; transactionType?: string; + transactionName?: string; environment?: string; } -interface Props { +export interface Props { ruleParams: RuleParams; metadata?: AlertMetadata; setRuleParams: (key: string, value: any) => void; @@ -78,6 +80,7 @@ export function TransactionErrorRateRuleType(props: Props) { environment: params.environment, serviceName: params.serviceName, transactionType: params.transactionType, + transactionName: params.transactionName, interval, start, end, @@ -89,6 +92,7 @@ export function TransactionErrorRateRuleType(props: Props) { }, [ params.transactionType, + params.transactionName, params.environment, params.serviceName, params.windowSize, @@ -102,7 +106,8 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => { if (value !== params.serviceName) { setRuleParams('serviceName', value); - setRuleParams('transactionType', ''); + setRuleParams('transactionType', undefined); + setRuleParams('transactionName', undefined); setRuleParams('environment', ENVIRONMENT_ALL.value); } }} @@ -117,6 +122,11 @@ export function TransactionErrorRateRuleType(props: Props) { onChange={(value) => setRuleParams('environment', value)} serviceName={params.serviceName} />, + setRuleParams('transactionName', value)} + serviceName={params.serviceName} + />, + + + + + + + + + + + diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx index ca9142668a65a..f5a647d3ca488 100644 --- a/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/service_icons/index.tsx @@ -16,9 +16,12 @@ import { getServerlessIcon } from '../agent_icon/get_serverless_icon'; import { CloudDetails } from './cloud_details'; import { ServerlessDetails } from './serverless_details'; import { ContainerDetails } from './container_details'; +import { OTelDetails } from './otel_details'; import { IconPopover } from './icon_popover'; import { ServiceDetails } from './service_details'; import { ServerlessType } from '../../../../common/serverless'; +import { isOpenTelemetryAgentName } from '../../../../common/agent_name'; +import openTelemetryIcon from '../agent_icon/icons/opentelemetry.svg'; interface Props { serviceName: string; @@ -70,7 +73,13 @@ export function getContainerIcon(container?: ContainerType) { } } -type Icons = 'service' | 'container' | 'serverless' | 'cloud' | 'alerts'; +type Icons = + | 'service' + | 'opentelemetry' + | 'container' + | 'serverless' + | 'cloud' + | 'alerts'; export interface PopoverItem { key: Icons; @@ -142,6 +151,23 @@ export function ServiceIcons({ start, end, serviceName }: Props) { }), component: , }, + { + key: 'opentelemetry', + icon: { + type: openTelemetryIcon, + }, + isVisible: + !!icons?.agentName && isOpenTelemetryAgentName(icons.agentName), + title: i18n.translate('xpack.apm.serviceIcons.opentelemetry', { + defaultMessage: 'OpenTelemetry', + }), + component: ( + + ), + }, { key: 'container', icon: { diff --git a/x-pack/plugins/apm/public/components/shared/service_icons/otel_details.tsx b/x-pack/plugins/apm/public/components/shared/service_icons/otel_details.tsx new file mode 100644 index 0000000000000..a9d2e8a963cac --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/service_icons/otel_details.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiDescriptionList, EuiDescriptionListProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { APIReturnType } from '../../../services/rest/create_call_apm_api'; + +type ServiceDetailsReturnType = + APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>; + +interface Props { + opentelemetry: ServiceDetailsReturnType['opentelemetry']; + agentName?: string; +} + +export function OTelDetails({ opentelemetry }: Props) { + if (!opentelemetry) { + return null; + } + + const listItems: EuiDescriptionListProps['listItems'] = []; + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceIcons.otelDetails.opentelemetry.language', + { + defaultMessage: 'Language', + } + ), + description: ( + <>{!!opentelemetry.language ? opentelemetry.language : 'unknown'} + ), + }); + + if (!!opentelemetry.sdkVersion) { + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceIcons.otelDetails.opentelemetry.sdkVersion', + { + defaultMessage: 'OTel SDK version', + } + ), + description: <>{opentelemetry.sdkVersion}, + }); + } + + if (!!opentelemetry.autoVersion) { + listItems.push({ + title: i18n.translate( + 'xpack.apm.serviceIcons.otelDetails.opentelemetry.autoVersion', + { + defaultMessage: 'Auto instrumentation agent version', + } + ), + description: <>{opentelemetry.autoVersion}, + }); + } + + return ; +} diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index 2527a11e3f001..695a9a579a35d 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -113,8 +113,12 @@ export function registerErrorCountRuleType({ }, }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, - ...termQuery(SERVICE_NAME, ruleParams.serviceName), - ...termQuery(ERROR_GROUP_ID, ruleParams.errorGroupingKey), + ...termQuery(SERVICE_NAME, ruleParams.serviceName, { + queryEmptyString: false, + }), + ...termQuery(ERROR_GROUP_ID, ruleParams.errorGroupingKey, { + queryEmptyString: false, + }), ...environmentQuery(ruleParams.environment), ], }, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 9df114c9a8c8c..9bb21324c22b7 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -148,9 +148,15 @@ export function registerTransactionDurationRuleType({ ...getDocumentTypeFilterForTransactions( searchAggregatedTransactions ), - ...termQuery(SERVICE_NAME, ruleParams.serviceName), - ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType), - ...termQuery(TRANSACTION_NAME, ruleParams.transactionName), + ...termQuery(SERVICE_NAME, ruleParams.serviceName, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_NAME, ruleParams.transactionName, { + queryEmptyString: false, + }), ...environmentQuery(ruleParams.environment), ] as QueryDslQueryContainer[], }, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts index ad27b5680b15c..3996ad5994710 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/get_transaction_error_rate_chart_preview.ts @@ -9,6 +9,7 @@ import { rangeQuery, termQuery } from '@kbn/observability-plugin/server'; import { SERVICE_NAME, TRANSACTION_TYPE, + TRANSACTION_NAME, } from '../../../../../common/es_fields/apm'; import { environmentQuery } from '../../../../../common/utils/environment_query'; import { AlertParams } from '../../route'; @@ -39,8 +40,15 @@ export async function getTransactionErrorRateChartPreview({ apmEventClient: APMEventClient; alertParams: AlertParams; }): Promise { - const { serviceName, environment, transactionType, interval, start, end } = - alertParams; + const { + serviceName, + environment, + transactionType, + interval, + start, + end, + transactionName, + } = alertParams; const searchAggregatedTransactions = await getSearchTransactionsEvents({ config, @@ -62,6 +70,7 @@ export async function getTransactionErrorRateChartPreview({ filter: [ ...termQuery(SERVICE_NAME, serviceName), ...termQuery(TRANSACTION_TYPE, transactionType), + ...termQuery(TRANSACTION_NAME, transactionName), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions( diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 7ceaf8ca78048..26b5847a205f1 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -32,6 +32,7 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME, TRANSACTION_TYPE, + TRANSACTION_NAME, } from '../../../../../common/es_fields/apm'; import { EventOutcome } from '../../../../../common/event_outcome'; import { @@ -86,6 +87,7 @@ export function registerTransactionErrorRateRuleType({ apmActionVariables.interval, apmActionVariables.reason, apmActionVariables.serviceName, + apmActionVariables.transactionName, apmActionVariables.threshold, apmActionVariables.transactionType, apmActionVariables.triggerValue, @@ -142,8 +144,15 @@ export function registerTransactionErrorRateRuleType({ ], }, }, - ...termQuery(SERVICE_NAME, ruleParams.serviceName), - ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType), + ...termQuery(SERVICE_NAME, ruleParams.serviceName, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_TYPE, ruleParams.transactionType, { + queryEmptyString: false, + }), + ...termQuery(TRANSACTION_NAME, ruleParams.transactionName, { + queryEmptyString: false, + }), ...environmentQuery(ruleParams.environment), ], }, @@ -232,6 +241,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, transactionType, environment, + ruleParams.transactionName, ] .filter((name) => name) .join('_'); @@ -255,6 +265,7 @@ export function registerTransactionErrorRateRuleType({ [SERVICE_NAME]: serviceName, ...getEnvironmentEsField(environment), [TRANSACTION_TYPE]: transactionType, + [TRANSACTION_NAME]: ruleParams.transactionName, [PROCESSOR_EVENT]: ProcessorEvent.transaction, [ALERT_EVALUATION_VALUE]: errorRate, [ALERT_EVALUATION_THRESHOLD]: ruleParams.threshold, @@ -272,6 +283,7 @@ export function registerTransactionErrorRateRuleType({ serviceName, threshold: ruleParams.threshold, transactionType, + transactionName: ruleParams.transactionName, triggerValue: asDecimalOrInteger(errorRate), viewInAppUrl, }); diff --git a/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts b/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts index 4563bd851f8a8..2231caa7df71e 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_metadata_details.ts @@ -24,17 +24,18 @@ import { SERVICE_VERSION, FAAS_ID, FAAS_TRIGGER_TYPE, + LABEL_TELEMETRY_AUTO_VERSION, } from '../../../common/es_fields/apm'; import { ContainerType } from '../../../common/service_metadata'; import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; -import { getProcessorEventForTransactions } from '../../lib/helpers/transactions'; import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client'; import { should } from './get_service_metadata_icons'; +import { isOpenTelemetryAgentName } from '../../../common/agent_name'; type ServiceMetadataDetailsRaw = Pick< TransactionRaw, - 'service' | 'agent' | 'host' | 'container' | 'kubernetes' | 'cloud' + 'service' | 'agent' | 'host' | 'container' | 'kubernetes' | 'cloud' | 'labels' >; export interface ServiceMetadataDetails { @@ -50,6 +51,11 @@ export interface ServiceMetadataDetails { version: string; }; }; + opentelemetry?: { + language?: string; + sdkVersion?: string; + autoVersion?: string; + }; container?: { ids?: string[]; image?: string; @@ -81,13 +87,11 @@ export interface ServiceMetadataDetails { export async function getServiceMetadataDetails({ serviceName, apmEventClient, - searchAggregatedTransactions, start, end, }: { serviceName: string; apmEventClient: APMEventClient; - searchAggregatedTransactions: boolean; start: number; end: number; }): Promise { @@ -99,16 +103,27 @@ export async function getServiceMetadataDetails({ const params = { apm: { events: [ - getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.transaction, ProcessorEvent.error, ProcessorEvent.metric, ], }, - sort: [{ '@timestamp': { order: 'desc' as const } }], + sort: [ + { _score: { order: 'desc' as const } }, + { '@timestamp': { order: 'desc' as const } }, + ], body: { track_total_hits: 1, size: 1, - _source: [SERVICE, AGENT, HOST, CONTAINER, KUBERNETES, CLOUD], + _source: [ + SERVICE, + AGENT, + HOST, + CONTAINER, + KUBERNETES, + CLOUD, + LABEL_TELEMETRY_AUTO_VERSION, + ], query: { bool: { filter, should } }, aggs: { serviceVersions: { @@ -178,8 +193,8 @@ export async function getServiceMetadataDetails({ }; } - const { service, agent, host, kubernetes, container, cloud } = response.hits - .hits[0]._source as ServiceMetadataDetailsRaw; + const { service, agent, host, kubernetes, container, cloud, labels } = + response.hits.hits[0]._source as ServiceMetadataDetailsRaw; const serviceMetadataDetails = { versions: response.aggregations?.serviceVersions.buckets.map( @@ -190,6 +205,17 @@ export async function getServiceMetadataDetails({ agent, }; + const otelDetails = + !!agent?.name && isOpenTelemetryAgentName(agent.name) + ? { + language: agent.name.startsWith('opentelemetry') + ? agent.name.replace(/^opentelemetry\//, '') + : undefined, + sdkVersion: agent?.version, + autoVersion: labels?.telemetry_auto_version as string, + } + : undefined; + const totalNumberInstances = response.aggregations?.totalNumberInstances.value; @@ -238,6 +264,7 @@ export async function getServiceMetadataDetails({ return { service: serviceMetadataDetails, + opentelemetry: otelDetails, container: containerDetails, serverless: serverlessDetails, cloud: cloudDetails, diff --git a/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts b/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts index 4890648a27ad3..37214b362e125 100644 --- a/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts +++ b/x-pack/plugins/apm/server/routes/services/get_service_metadata_icons.ts @@ -16,6 +16,9 @@ import { SERVICE_NAME, KUBERNETES_POD_NAME, HOST_OS_PLATFORM, + LABEL_TELEMETRY_AUTO_VERSION, + AGENT_VERSION, + SERVICE_FRAMEWORK_NAME, } from '../../../common/es_fields/apm'; import { ContainerType } from '../../../common/service_metadata'; import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw'; @@ -44,6 +47,9 @@ export const should = [ { exists: { field: CLOUD_PROVIDER } }, { exists: { field: HOST_OS_PLATFORM } }, { exists: { field: AGENT_NAME } }, + { exists: { field: AGENT_VERSION } }, + { exists: { field: SERVICE_FRAMEWORK_NAME } }, + { exists: { field: LABEL_TELEMETRY_AUTO_VERSION } }, ]; export async function getServiceMetadataIcons({ diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index 2b31be70ad6ba..9cddb4d3577f6 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -240,22 +240,13 @@ const serviceMetadataDetailsRoute = createApmServerRoute({ handler: async (resources): Promise => { const apmEventClient = await getApmEventClient(resources); const infraMetricsClient = createInfraMetricsClient(resources); - const { params, config } = resources; + const { params } = resources; const { serviceName } = params.path; const { start, end } = params.query; - const searchAggregatedTransactions = await getSearchTransactionsEvents({ - apmEventClient, - config, - start, - end, - kuery: '', - }); - const serviceMetadataDetails = await getServiceMetadataDetails({ serviceName, apmEventClient, - searchAggregatedTransactions, start, end, }); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx index b02df3a82228f..c1d7fe20bee48 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.test.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import type { AppMockRenderer } from '../../common/mock'; @@ -35,4 +36,23 @@ describe('FilePreview', () => { expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); }); + + it('pressing escape calls closePreview', async () => { + const closePreview = jest.fn(); + + appMockRender.render(); + + await waitFor(() => + expect(appMockRender.getFilesClient().getDownloadHref).toHaveBeenCalledWith({ + id: basicFileMock.id, + fileKind: constructFileKindIdByOwner(mockedTestProvidersOwner[0]), + }) + ); + + expect(await screen.findByTestId('cases-files-image-preview')).toBeInTheDocument(); + + userEvent.keyboard('{esc}'); + + await waitFor(() => expect(closePreview).toHaveBeenCalled()); + }); }); diff --git a/x-pack/plugins/cases/public/components/files/file_preview.tsx b/x-pack/plugins/cases/public/components/files/file_preview.tsx index 1bb91c5b53ff7..09cee1320ec2a 100644 --- a/x-pack/plugins/cases/public/components/files/file_preview.tsx +++ b/x-pack/plugins/cases/public/components/files/file_preview.tsx @@ -4,12 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import type { FileJSON } from '@kbn/shared-ux-file-types'; -import { EuiOverlayMask, EuiFocusTrap, EuiImage } from '@elastic/eui'; +import { EuiOverlayMask, EuiFocusTrap, EuiImage, keys } from '@elastic/eui'; import { useFilesContext } from '@kbn/shared-ux-file-context'; import type { Owner } from '../../../common/constants/types'; @@ -36,6 +36,20 @@ export const FilePreview = ({ closePreview, selectedFile }: FilePreviewProps) => const { client: filesClient } = useFilesContext(); const { owner } = useCasesContext(); + useEffect(() => { + const keyboardListener = (event: KeyboardEvent) => { + if (event.key === keys.ESCAPE || event.code === 'Escape') { + closePreview(); + } + }; + + window.addEventListener('keyup', keyboardListener); + + return () => { + window.removeEventListener('keyup', keyboardListener); + }; + }, [closePreview]); + return ( diff --git a/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx index 5b17b05a45f68..dccf5ae0b91a2 100644 --- a/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx +++ b/x-pack/plugins/cases/public/components/user_actions/comment/actions.tsx @@ -34,6 +34,7 @@ export const createActionAttachmentUserActionBuilder = ({ // TODO: Fix this manually. Issue #123375 // eslint-disable-next-line react/display-name build: () => { + const actionIconName = comment.actions.type === 'isolate' ? 'lock' : 'lockOpen'; return [ { username: ( @@ -52,7 +53,8 @@ export const createActionAttachmentUserActionBuilder = ({ ), 'data-test-subj': 'endpoint-action', timestamp: , - timelineAvatar: comment.actions.type === 'isolate' ? 'lock' : 'lockOpen', + timelineAvatar: actionIconName, + timelineAvatarAriaLabel: actionIconName, actions: , children: comment.comment.trim().length > 0 && ( diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts index 359aa621798f0..50b5b142fd319 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.test.ts @@ -565,6 +565,88 @@ describe('utils', () => { }); describe('files', () => { + it('rounds the average file size when it is a decimal', () => { + const attachmentFramework: AttachmentFrameworkAggsResult = { + externalReferenceTypes: { + buckets: [ + { + doc_count: 5, + key: '.files', + references: { + cases: { + max: { + value: 10, + }, + }, + }, + }, + ], + }, + persistableReferenceTypes: { + buckets: [], + }, + }; + + expect( + getAttachmentsFrameworkStats({ + attachmentAggregations: attachmentFramework, + totalCasesForOwner: 5, + filesAggregations: { + averageSize: { value: 1.1 }, + topMimeTypes: { + buckets: [], + }, + }, + }).attachmentFramework.files + ).toMatchInlineSnapshot(` + Object { + "average": 1, + "averageSize": 1, + "maxOnACase": 10, + "topMimeTypes": Array [], + "total": 5, + } + `); + }); + + it('sets the average file size to 0 when the aggregation does not exist', () => { + const attachmentFramework: AttachmentFrameworkAggsResult = { + externalReferenceTypes: { + buckets: [ + { + doc_count: 5, + key: '.files', + references: { + cases: { + max: { + value: 10, + }, + }, + }, + }, + ], + }, + persistableReferenceTypes: { + buckets: [], + }, + }; + + expect( + getAttachmentsFrameworkStats({ + attachmentAggregations: attachmentFramework, + totalCasesForOwner: 5, + }).attachmentFramework.files + ).toMatchInlineSnapshot(` + Object { + "average": 1, + "averageSize": 0, + "maxOnACase": 10, + "topMimeTypes": Array [], + "total": 5, + } + `); + }); + it('sets the files stats to empty when the file aggregation results is the empty version', () => { const attachmentFramework: AttachmentFrameworkAggsResult = { externalReferenceTypes: { diff --git a/x-pack/plugins/cases/server/telemetry/queries/utils.ts b/x-pack/plugins/cases/server/telemetry/queries/utils.ts index 7896c2bdac760..e47e9d1613bce 100644 --- a/x-pack/plugins/cases/server/telemetry/queries/utils.ts +++ b/x-pack/plugins/cases/server/telemetry/queries/utils.ts @@ -230,7 +230,7 @@ export const getAttachmentsFrameworkStats = ({ return emptyAttachmentFramework(); } - const averageFileSize = filesAggregations?.averageSize?.value; + const averageFileSize = getAverageFileSize(filesAggregations); const topMimeTypes = filesAggregations?.topMimeTypes; return { @@ -253,6 +253,14 @@ export const getAttachmentsFrameworkStats = ({ }; }; +const getAverageFileSize = (filesAggregations?: FileAttachmentAggsResult) => { + if (filesAggregations?.averageSize?.value == null) { + return 0; + } + + return Math.round(filesAggregations.averageSize.value); +}; + const getAttachmentRegistryStats = ( registryResults: BucketsWithMaxOnCase, totalCasesForOwner: number diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx similarity index 91% rename from x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx rename to x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx index c1b86258a46c9..c15e0ce87570f 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/cloud_posture_score_chart.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_charts/compliance_score_chart.tsx @@ -29,13 +29,14 @@ import { import { FormattedDate, FormattedTime } from '@kbn/i18n-react'; import moment from 'moment'; import { i18n } from '@kbn/i18n'; +import { DASHBOARD_COMPLIANCE_SCORE_CHART } from '../test_subjects'; import { statusColors } from '../../../common/constants'; import { RULE_FAILED, RULE_PASSED } from '../../../../common/constants'; import { CompactFormattedNumber } from '../../../components/compact_formatted_number'; import type { Evaluation, PostureTrend, Stats } from '../../../../common/types'; import { useKibana } from '../../../common/hooks/use_kibana'; -interface CloudPostureScoreChartProps { +interface ComplianceScoreChartProps { compact?: boolean; trend: PostureTrend[]; data: Stats; @@ -48,7 +49,7 @@ const getPostureScorePercentage = (postureScore: number): string => `${Math.roun const PercentageInfo = ({ compact, postureScore, -}: CloudPostureScoreChartProps['data'] & { compact?: CloudPostureScoreChartProps['compact'] }) => { +}: ComplianceScoreChartProps['data'] & { compact?: ComplianceScoreChartProps['compact'] }) => { const { euiTheme } = useEuiTheme(); const percentage = getPostureScorePercentage(postureScore); @@ -59,6 +60,7 @@ const PercentageInfo = ({ paddingLeft: compact ? euiTheme.size.s : euiTheme.size.xs, marginBottom: compact ? euiTheme.size.s : 'none', }} + data-test-subj={DASHBOARD_COMPLIANCE_SCORE_CHART.COMPLIANCE_SCORE} >

{percentage}

@@ -140,12 +142,12 @@ const CounterLink = ({ ); }; -export const CloudPostureScoreChart = ({ +export const ComplianceScoreChart = ({ data, trend, onEvalCounterClick, compact, -}: CloudPostureScoreChartProps) => { +}: ComplianceScoreChartProps) => { const { euiTheme } = useEuiTheme(); return ( @@ -173,7 +175,7 @@ export const CloudPostureScoreChart = ({ color={statusColors.passed} onClick={() => onEvalCounterClick(RULE_PASSED)} tooltipContent={i18n.translate( - 'xpack.csp.cloudPostureScoreChart.counterLink.passedFindingsTooltip', + 'xpack.csp.complianceScoreChart.counterLink.passedFindingsTooltip', { defaultMessage: 'Passed findings' } )} /> @@ -184,7 +186,7 @@ export const CloudPostureScoreChart = ({ color={statusColors.failed} onClick={() => onEvalCounterClick(RULE_FAILED)} tooltipContent={i18n.translate( - 'xpack.csp.cloudPostureScoreChart.counterLink.failedFindingsTooltip', + 'xpack.csp.complianceScoreChart.counterLink.failedFindingsTooltip', { defaultMessage: 'Failed findings' } )} /> diff --git a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx index 6c75891fc62af..fe4f62dbbc8f5 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/compliance_dashboard/compliance_dashboard.tsx @@ -31,6 +31,7 @@ import { KUBERNETES_DASHBOARD_CONTAINER, KUBERNETES_DASHBOARD_TAB, CLOUD_DASHBOARD_TAB, + CLOUD_POSTURE_DASHBOARD_PAGE_HEADER, } from './test_subjects'; import { useCspmStatsApi, useKspmStatsApi } from '../../common/api/use_stats_api'; import { useCspSetupStatusApi } from '../../common/api/use_setup_status_api'; @@ -332,6 +333,7 @@ export const ComplianceDashboard = () => { return ( - @@ -151,7 +152,7 @@ export const SummarySection = ({ - { return ( diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts index ced7bf1058762..3419b1674fddc 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/hooks/use_overall_stats.ts @@ -16,6 +16,7 @@ import type { IKibanaSearchResponse, ISearchOptions, } from '@kbn/data-plugin/common'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { useDataVisualizerKibana } from '../../kibana_context'; import { AggregatableFieldOverallStats, @@ -29,7 +30,6 @@ import { } from '../search_strategy/requests/overall_stats'; import type { OverallStats } from '../types/overall_stats'; import { getDefaultPageState } from '../components/index_data_visualizer_view/index_data_visualizer_view'; -import { extractErrorProperties } from '../utils/error_utils'; import { DataStatsFetchProgress, isRandomSamplingOption, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts index bf941e0952657..e394f6456d8f5 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_boolean_field_stats.ts @@ -15,6 +15,7 @@ import type { ISearchStart, } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; @@ -25,7 +26,6 @@ import type { FieldStatsCommonRequestParams, } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; export const getBooleanFieldsStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts index 863cd6885fe88..4bd914f3637e5 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_date_field_stats.ts @@ -16,11 +16,11 @@ import type { ISearchStart, } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; import type { FieldStatsCommonRequestParams } from '../../../../../common/types/field_stats'; import type { Field, DateFieldStats, Aggs } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; export const getDateFieldsStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts index df7afb16479f0..3943979d290d9 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_field_examples.ts @@ -17,6 +17,7 @@ import type { import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { SearchHit } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { buildBaseFilterCriteria } from '@kbn/ml-query-utils'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { getUniqGeoOrStrExamples } from '../../../common/util/example_utils'; import type { Field, @@ -24,7 +25,6 @@ import type { FieldStatsCommonRequestParams, } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; import { MAX_EXAMPLES_DEFAULT } from './constants'; export const getFieldExamplesRequest = (params: FieldStatsCommonRequestParams, field: Field) => { diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts index b9dd55351781f..f7d1b39f15d3f 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_numeric_field_stats.ts @@ -18,6 +18,7 @@ import type { import type { ISearchStart } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { isDefined } from '@kbn/ml-is-defined'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; import { MAX_PERCENT, PERCENTILE_SPACING, SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; @@ -32,7 +33,6 @@ import type { FieldStatsError, } from '../../../../../common/types/field_stats'; import { processDistributionData } from '../../utils/process_distribution_data'; -import { extractErrorProperties } from '../../utils/error_utils'; import { isIKibanaSearchResponse, isNormalSamplingOption, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts index a035842fa8767..159be48b338e4 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/search_strategy/requests/get_string_field_stats.ts @@ -16,6 +16,7 @@ import type { ISearchStart, } from '@kbn/data-plugin/public'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { processTopValues } from './utils'; import { buildAggregationWithSamplingOption } from './build_random_sampler_agg'; import { SAMPLER_TOP_TERMS_THRESHOLD } from './constants'; @@ -26,7 +27,6 @@ import type { StringFieldStats, } from '../../../../../common/types/field_stats'; import { FieldStatsError, isIKibanaSearchResponse } from '../../../../../common/types/field_stats'; -import { extractErrorProperties } from '../../utils/error_utils'; export const getStringFieldStatsRequest = ( params: FieldStatsCommonRequestParams, diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts deleted file mode 100644 index 06a9b0b4002a6..0000000000000 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts +++ /dev/null @@ -1,184 +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 type { IHttpFetchError } from '@kbn/core-http-browser'; -import Boom from '@hapi/boom'; -import { isPopulatedObject } from '@kbn/ml-is-populated-object'; - -export interface WrappedError { - body: { - attributes: { - body: EsErrorBody; - }; - message: Boom.Boom; - }; - statusCode: number; -} - -export interface EsErrorRootCause { - type: string; - reason: string; - caused_by?: EsErrorRootCause; - script?: string; -} - -export interface EsErrorBody { - error: { - root_cause?: EsErrorRootCause[]; - caused_by?: EsErrorRootCause; - type: string; - reason: string; - }; - status: number; -} - -export interface DVResponseError { - statusCode: number; - error: string; - message: string; - attributes?: { - body: EsErrorBody; - }; -} - -export interface ErrorMessage { - message: string; -} - -export interface DVErrorObject { - causedBy?: string; - message: string; - statusCode?: number; - fullError?: EsErrorBody; -} - -export interface DVHttpFetchError extends IHttpFetchError { - body: T; -} - -export type ErrorType = - | WrappedError - | DVHttpFetchError - | EsErrorBody - | Boom.Boom - | string - | undefined; - -export function isEsErrorBody(error: any): error is EsErrorBody { - return error && error.error?.reason !== undefined; -} - -export function isErrorString(error: any): error is string { - return typeof error === 'string'; -} - -export function isErrorMessage(error: any): error is ErrorMessage { - return error && error.message !== undefined && typeof error.message === 'string'; -} - -export function isDVResponseError(error: any): error is DVResponseError { - return typeof error.body === 'object' && 'message' in error.body; -} - -export function isBoomError(error: any): error is Boom.Boom { - return error?.isBoom === true; -} - -export function isWrappedError(error: any): error is WrappedError { - return error && isBoomError(error.body?.message) === true; -} - -export const extractErrorProperties = (error: ErrorType): DVErrorObject => { - // extract properties of the error object from within the response error - // coming from Kibana, Elasticsearch, and our own DV messages - - // some responses contain raw es errors as part of a bulk response - // e.g. if some jobs fail the action in a bulk request - - if (isEsErrorBody(error)) { - return { - message: error.error.reason, - statusCode: error.status, - fullError: error, - }; - } - - if (isErrorString(error)) { - return { - message: error, - }; - } - if (isWrappedError(error)) { - return error.body.message?.output?.payload; - } - - if (isBoomError(error)) { - return { - message: error.output.payload.message, - statusCode: error.output.payload.statusCode, - }; - } - - if (error?.body === undefined && !error?.message) { - return { - message: '', - }; - } - - if (typeof error.body === 'string') { - return { - message: error.body, - }; - } - - if (isDVResponseError(error)) { - if ( - typeof error.body.attributes === 'object' && - typeof error.body.attributes.body?.error?.reason === 'string' - ) { - const errObj: DVErrorObject = { - message: error.body.attributes.body.error.reason, - statusCode: error.body.statusCode, - fullError: error.body.attributes.body, - }; - if ( - typeof error.body.attributes.body.error.caused_by === 'object' && - (typeof error.body.attributes.body.error.caused_by?.reason === 'string' || - typeof error.body.attributes.body.error.caused_by?.caused_by?.reason === 'string') - ) { - errObj.causedBy = - error.body.attributes.body.error.caused_by?.caused_by?.reason || - error.body.attributes.body.error.caused_by?.reason; - } - if ( - Array.isArray(error.body.attributes.body.error.root_cause) && - typeof error.body.attributes.body.error.root_cause[0] === 'object' && - isPopulatedObject(error.body.attributes.body.error.root_cause[0], ['script']) - ) { - errObj.causedBy = error.body.attributes.body.error.root_cause[0].script; - errObj.message += `: '${error.body.attributes.body.error.root_cause[0].script}'`; - } - return errObj; - } else { - return { - message: error.body.message, - statusCode: error.body.statusCode, - }; - } - } - - if (isErrorMessage(error)) { - return { - message: error.message, - }; - } - - // If all else fail return an empty message instead of JSON.stringify - return { - message: '', - }; -}; diff --git a/x-pack/plugins/data_visualizer/tsconfig.json b/x-pack/plugins/data_visualizer/tsconfig.json index 7f38e7b9b77cb..4609d1e9497d2 100644 --- a/x-pack/plugins/data_visualizer/tsconfig.json +++ b/x-pack/plugins/data_visualizer/tsconfig.json @@ -17,7 +17,6 @@ "@kbn/cloud-chat-plugin", "@kbn/cloud-plugin", "@kbn/core-execution-context-common", - "@kbn/core-http-browser", "@kbn/core", "@kbn/custom-integrations-plugin", "@kbn/data-plugin", @@ -60,6 +59,7 @@ "@kbn/unified-search-plugin", "@kbn/usage-collection-plugin", "@kbn/utility-types", + "@kbn/ml-error-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts b/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts index 8d962ee206f9e..78a720304d996 100644 --- a/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts +++ b/x-pack/plugins/enterprise_search/common/connectors/native_connectors.ts @@ -27,6 +27,7 @@ export const NATIVE_CONNECTOR_DEFINITIONS: Record { 'xpack.enterpriseSearch.overview.elasticsearchGuide.elasticsearchDescription', { defaultMessage: - 'Whether you are building a search-powered application, or designing a large-scale search implementation, Elasticsearch provides the low-level tools to create the most relevant and performant search experience.', + "Elasticsearch provides the low-level tools you need to build fast, relevant search for your website or application. Because it's powerful and flexible, Elasticsearch can handle search use cases of all shapes and sizes.", } )}

@@ -103,7 +103,7 @@ export const ElasticsearchGuide: React.FC = () => { 'xpack.enterpriseSearch.overview.elasticsearchGuide.connectToElasticsearchDescription', { defaultMessage: - "Elastic builds and maintains clients in several popular languages and our community has contributed many more. They're easy to work with, feel natural to use, and, just like Elasticsearch, don't limit what you might want to do with them.", + 'Elastic builds and maintains clients in several popular languages and our community has contributed many more.', } )}

diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts index 5ae2784b52664..1a4a58558dc5c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/search_indices.mock.ts @@ -45,6 +45,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, }, @@ -155,6 +156,7 @@ export const indices: ElasticsearchIndexWithIngestion[] = [ required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts index 1674f5ca18675..3f9972d1545c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/__mocks__/view_index.mock.ts @@ -55,6 +55,7 @@ export const connectorIndex: ConnectorViewIndex = { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, }, @@ -169,6 +170,7 @@ export const crawlerIndex: CrawlerViewIndex = { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx index b61614838d7a1..2fe691e262b64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_api_integration.tsx @@ -12,23 +12,24 @@ import { useValues } from 'kea'; import { EuiCodeBlock, EuiSpacer, EuiText, EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; - +import { useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; import { EngineViewLogic } from '../engine_view_logic'; import { EngineApiLogic } from './engine_api_logic'; -const SearchUISnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` +import { elasticsearchUrl } from './search_application_api'; + +const SearchUISnippet = (esUrl: string, engineName: string, apiKey: string) => `6 import EnginesAPIConnector from "@elastic/search-ui-engines-connector"; const connector = new EnginesAPIConnector({ - host: "${enterpriseSearchUrl}", + host: "${esUrl}", engineName: "${engineName}", apiKey: "${apiKey || ''}" });`; -const cURLSnippet = (enterpriseSearchUrl: string, engineName: string, apiKey: string) => ` -curl --location --request GET '${enterpriseSearchUrl}/api/engines/${engineName}/_search' \\ +const cURLSnippet = (esUrl: string, engineName: string, apiKey: string) => ` +curl --location --request GET '${esUrl}/${engineName}/_search' \\ --header 'Authorization: apiKey ${apiKey || ''}' \\ --header 'Content-Type: application/json' \\ --data-raw '{ @@ -47,19 +48,19 @@ interface Tab { export const EngineApiIntegrationStage: React.FC = () => { const [selectedTab, setSelectedTab] = React.useState('curl'); const { engineName } = useValues(EngineViewLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { apiKey } = useValues(EngineApiLogic); + const cloudContext = useCloudDetails(); const Tabs: Record = { curl: { - code: cURLSnippet(enterpriseSearchUrl, engineName, apiKey), + code: cURLSnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'bash', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.curlTitle', { defaultMessage: 'cURL', }), }, searchui: { - code: SearchUISnippet(enterpriseSearchUrl, engineName, apiKey), + code: SearchUISnippet(elasticsearchUrl(cloudContext), engineName, apiKey), language: 'javascript', title: i18n.translate('xpack.enterpriseSearch.content.engine.api.step3.searchUITitle', { defaultMessage: 'Search UI', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx index 2dd55304b6035..a6c29285f4f66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/engine_connect.tsx @@ -15,7 +15,7 @@ import { i18n } from '@kbn/i18n'; import { generateEncodedPath } from '../../../../shared/encode_path_params'; import { KibanaLogic } from '../../../../shared/kibana'; import { - SEARCH_APPLICATION_CONNECT_PATH, + SEARCH_APPLICATION_CONTENT_PATH, EngineViewTabs, SearchApplicationConnectTabs, } from '../../../routes'; @@ -55,7 +55,7 @@ export const EngineConnect: React.FC = () => { }>(); const onTabClick = (tab: SearchApplicationConnectTabs) => () => { KibanaLogic.values.navigateToUrl( - generateEncodedPath(SEARCH_APPLICATION_CONNECT_PATH, { + generateEncodedPath(SEARCH_APPLICATION_CONTENT_PATH, { engineName, connectTabId: tab, }) diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx index 9d3c27895657f..6934de4051bdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_connect/search_application_api.tsx @@ -23,9 +23,10 @@ import { i18n } from '@kbn/i18n'; import { ANALYTICS_PLUGIN } from '../../../../../../common/constants'; import { COLLECTION_INTEGRATE_PATH } from '../../../../analytics/routes'; +import { CloudDetails, useCloudDetails } from '../../../../shared/cloud_details/cloud_details'; +import { decodeCloudId } from '../../../../shared/decode_cloud_id/decode_cloud_id'; import { docLinks } from '../../../../shared/doc_links'; import { generateEncodedPath } from '../../../../shared/encode_path_params'; -import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; import { KibanaLogic } from '../../../../shared/kibana'; import { EngineViewLogic } from '../engine_view_logic'; @@ -34,12 +35,19 @@ import { EngineApiIntegrationStage } from './engine_api_integration'; import { EngineApiLogic } from './engine_api_logic'; import { GenerateEngineApiKeyModal } from './generate_engine_api_key_modal/generate_engine_api_key_modal'; +export const elasticsearchUrl = (cloudContext: CloudDetails): string => { + const defaultUrl = 'https://localhost:9200'; + const url = + (cloudContext.cloudId && decodeCloudId(cloudContext.cloudId)?.elasticsearchUrl) || defaultUrl; + return url; +}; + export const SearchApplicationAPI = () => { const { engineName } = useValues(EngineViewLogic); const { isGenerateModalOpen } = useValues(EngineApiLogic); const { openGenerateModal, closeGenerateModal } = useActions(EngineApiLogic); - const enterpriseSearchUrl = getEnterpriseSearchUrl(); const { navigateToUrl } = useValues(KibanaLogic); + const cloudContext = useCloudDetails(); const steps = [ { @@ -132,7 +140,7 @@ export const SearchApplicationAPI = () => { - {enterpriseSearchUrl} + {elasticsearchUrl(cloudContext)} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx index 4bcd3fcf4f215..285846f7ccb1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_indices.tsx @@ -11,7 +11,6 @@ import { useActions, useValues } from 'kea'; import { EuiBasicTableColumn, - EuiButton, EuiCallOut, EuiConfirmModal, EuiIcon, @@ -32,24 +31,15 @@ import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry/telemetry_logic'; -import { SEARCH_INDEX_PATH, EngineViewTabs } from '../../routes'; +import { SEARCH_INDEX_PATH } from '../../routes'; -import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; - -import { AddIndicesFlyout } from './add_indices_flyout'; import { EngineIndicesLogic } from './engine_indices_logic'; -const pageTitle = i18n.translate('xpack.enterpriseSearch.content.engine.indices.pageTitle', { - defaultMessage: 'Indices', -}); - export const EngineIndices: React.FC = () => { const subduedBackground = useEuiBackgroundColor('subdued'); const { sendEnterpriseSearchTelemetry } = useActions(TelemetryLogic); - const { engineData, engineName, isLoadingEngine, addIndicesFlyoutOpen } = - useValues(EngineIndicesLogic); - const { removeIndexFromEngine, openAddIndicesFlyout, closeAddIndicesFlyout } = - useActions(EngineIndicesLogic); + const { engineData } = useValues(EngineIndicesLogic); + const { removeIndexFromEngine } = useActions(EngineIndicesLogic); const { navigateToUrl } = useValues(KibanaLogic); const [removeIndexConfirm, setConfirmRemoveIndex] = useState(null); @@ -177,116 +167,92 @@ export const EngineIndices: React.FC = () => { }, ]; return ( - + {hasUnknownIndices && ( + <> + - {i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', { - defaultMessage: 'Add new indices', - })} - , - ], - }} - engineName={engineName} - > - <> - {hasUnknownIndices && ( - <> - + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.description', + { + defaultMessage: + 'Some data might be unreachable from this search application. Check for any pending operations or errors on affected indices, or remove indices that should no longer be used by this search application.', + } )} - > -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.unknownIndicesCallout.description', - { - defaultMessage: - 'Some data might be unreachable from this search application. Check for any pending operations or errors on affected indices, or remove those that should no longer be used by this search application.', - } - )} -

-
- - - )} - { - if (index.health === 'unknown') { - return { style: { backgroundColor: subduedBackground } }; - } +

+
+ + + )} + { + if (index.health === 'unknown') { + return { style: { backgroundColor: subduedBackground } }; + } - return {}; - }} - search={{ - box: { - incremental: true, - placeholder: i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.searchPlaceholder', - { defaultMessage: 'Filter indices' } - ), - schema: true, - }, + return {}; + }} + search={{ + box: { + incremental: true, + placeholder: i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.searchPlaceholder', + { defaultMessage: 'Filter indices' } + ), + schema: true, + }, + }} + pagination + sorting + /> + {removeIndexConfirm !== null && ( + setConfirmRemoveIndex(null)} + onConfirm={() => { + removeIndexFromEngine(removeIndexConfirm); + setConfirmRemoveIndex(null); + sendEnterpriseSearchTelemetry({ + action: 'clicked', + metric: 'entSearchContent-engines-indices-removeIndexConfirm', + }); }} - pagination - sorting - /> - {removeIndexConfirm !== null && ( - setConfirmRemoveIndex(null)} - onConfirm={() => { - removeIndexFromEngine(removeIndexConfirm); - setConfirmRemoveIndex(null); - sendEnterpriseSearchTelemetry({ - action: 'clicked', - metric: 'entSearchContent-engines-indices-removeIndexConfirm', - }); - }} - title={i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title', - { defaultMessage: 'Remove this index from the Search Application' } - )} - buttonColor="danger" - cancelButtonText={CANCEL_BUTTON_LABEL} - confirmButtonText={i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text', - { - defaultMessage: 'Yes, Remove This Index', - } - )} - defaultFocusedButton="confirm" - maxWidth - > - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description', - { - defaultMessage: - "This won't delete the index. You may add it back to this search application at a later time.", - } - )} -

-
-
- )} - {addIndicesFlyoutOpen && } - -
+ title={i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.title', + { defaultMessage: 'Remove this index from the search application' } + )} + buttonColor="danger" + cancelButtonText={CANCEL_BUTTON_LABEL} + confirmButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.text', + { + defaultMessage: 'Yes, Remove This Index', + } + )} + defaultFocusedButton="confirm" + maxWidth + > + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.engine.indices.removeIndexConfirm.description', + { + defaultMessage: + "This won't delete the index. You may add it back to this search application at a later time.", + } + )} +

+
+ + )} + ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx index 4ceba9cccb142..fd10624188ff0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_schema.tsx @@ -42,8 +42,7 @@ import { docLinks } from '../../../shared/doc_links'; import { generateEncodedPath } from '../../../shared/encode_path_params'; import { KibanaLogic } from '../../../shared/kibana'; import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { EngineViewTabs, SEARCH_INDEX_TAB_PATH } from '../../routes'; -import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_template'; +import { SEARCH_INDEX_TAB_PATH } from '../../routes'; import { EngineIndicesLogic } from './engine_indices_logic'; @@ -153,10 +152,6 @@ const SchemaFieldDetails: React.FC<{ schemaField: SchemaField }> = ({ schemaFiel ); }; -const pageTitle = i18n.translate('xpack.enterpriseSearch.content.engine.schema.pageTitle', { - defaultMessage: 'Schema', -}); - export const EngineSchema: React.FC = () => { const { engineName } = useValues(EngineIndicesLogic); const [onlyShowConflicts, setOnlyShowConflicts] = useState(false); @@ -348,125 +343,115 @@ export const EngineSchema: React.FC = () => { ); return ( - - <> - - - - - - {i18n.translate('xpack.enterpriseSearch.content.engine.schema.filters.label', { - defaultMessage: 'Filter By', - })} - - - setIsFilterByPopoverOpen(false)} - panelPaddingSize="none" - anchorPosition="downCenter" + <> + + + + + + {i18n.translate('xpack.enterpriseSearch.content.engine.schema.filters.label', { + defaultMessage: 'Filter By', + })} + + + setIsFilterByPopoverOpen(false)} + panelPaddingSize="none" + anchorPosition="downCenter" + > + setSelectedEsFieldTypes(options)} > - ( +
+ {search} + {list} +
+ )} +
+ + + setSelectedEsFieldTypes(esFieldTypes)} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.clearAll', { - defaultMessage: 'Filter list ', + defaultMessage: 'Clear all ', } - ), - }} - options={selectedEsFieldTypes} - onChange={(options) => setSelectedEsFieldTypes(options)} - > - {(list, search) => ( -
- {search} - {list} -
- )} -
- - - setSelectedEsFieldTypes(esFieldTypes)} - > - {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.clearAll', - { - defaultMessage: 'Clear all ', - } - )} - - - -
-
-
+ )} + +
+ +
+
- - - {totalConflictsHiddenByTypeFilters > 0 && ( - - } - color="danger" - iconType="iInCircle" - > -

- {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.subTitle', - { - defaultMessage: - 'In order to see all field conflicts you must clear your field filters', - } - )} -

- setSelectedEsFieldTypes(esFieldTypes)}> - {i18n.translate( - 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.clearFilters', - { - defaultMessage: 'Clear filters ', - } - )} - -
- )}
- -
+ + + {totalConflictsHiddenByTypeFilters > 0 && ( + + } + color="danger" + iconType="iInCircle" + > +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.subTitle', + { + defaultMessage: + 'In order to see all field conflicts you must clear your field filters', + } + )} +

+ setSelectedEsFieldTypes(esFieldTypes)}> + {i18n.translate( + 'xpack.enterpriseSearch.content.engine.schema.filters.conflict.callout.clearFilters', + { + defaultMessage: 'Clear filters ', + } + )} + +
+ )} +
+ ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx index 35e56f825d33a..6bbfe9b1de967 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engine/engine_view.tsx @@ -17,9 +17,11 @@ import { Status } from '../../../../../common/types/api'; import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_PATH, + SEARCH_APPLICATION_CONTENT_PATH, SEARCH_APPLICATION_CONNECT_PATH, EngineViewTabs, SearchApplicationConnectTabs, + SearchApplicationContentTabs, } from '../../routes'; import { DeleteEngineModal } from '../engines/delete_engine_modal'; @@ -27,11 +29,10 @@ import { EnterpriseSearchEnginesPageTemplate } from '../layout/engines_page_temp import { EngineConnect } from './engine_connect/engine_connect'; import { EngineError } from './engine_error'; -import { EngineIndices } from './engine_indices'; -import { EngineSchema } from './engine_schema'; import { EngineSearchPreview } from './engine_search_preview/engine_search_preview'; import { EngineViewLogic } from './engine_view_logic'; import { EngineHeaderDocsAction } from './header_docs_action'; +import { SearchApplicationContent } from './search_application_content'; export const EngineView: React.FC = () => { const { fetchEngine, closeDeleteEngineModal } = useActions(EngineViewLogic); @@ -74,8 +75,11 @@ export const EngineView: React.FC = () => { path={`${ENGINE_PATH}/${EngineViewTabs.PREVIEW}`} component={EngineSearchPreview} /> - - + + { + switch (tabId) { + case SearchApplicationContentTabs.INDICES: + return INDICES_TAB_TITLE; + case SearchApplicationContentTabs.SCHEMA: + return SCHEMA_TAB_TITLE; + default: + return tabId; + } +}; + +const ContentTabs: string[] = Object.values(SearchApplicationContentTabs); + +export const SearchApplicationContent = () => { + const { engineName, isLoadingEngine } = useValues(EngineViewLogic); + const { addIndicesFlyoutOpen } = useValues(EngineIndicesLogic); + const { closeAddIndicesFlyout, openAddIndicesFlyout } = useActions(EngineIndicesLogic); + const { contentTabId = SearchApplicationContentTabs.INDICES } = useParams<{ + contentTabId?: string; + }>(); + + if (!ContentTabs.includes(contentTabId)) { + return ( + + + + ); + } + + const onTabClick = (tab: SearchApplicationContentTabs) => () => { + KibanaLogic.values.navigateToUrl( + generateEncodedPath(SEARCH_APPLICATION_CONTENT_PATH, { + contentTabId: tab, + engineName, + }) + ); + }; + + return ( + + KibanaLogic.values.navigateToUrl( + generateEncodedPath(ENGINE_PATH, { + engineName, + }) + ), + text: ( + <> + {engineName} + + ), + }, + ], + pageTitle, + rightSideItems: [ + + {i18n.translate('xpack.enterpriseSearch.content.engine.indices.addNewIndicesButton', { + defaultMessage: 'Add new indices', + })} + , + ], + tabs: [ + { + isSelected: contentTabId === SearchApplicationContentTabs.INDICES, + label: INDICES_TAB_TITLE, + onClick: onTabClick(SearchApplicationContentTabs.INDICES), + }, + { + isSelected: contentTabId === SearchApplicationContentTabs.SCHEMA, + label: SCHEMA_TAB_TITLE, + onClick: onTabClick(SearchApplicationContentTabs.SCHEMA), + }, + ], + }} + engineName={engineName} + > + {contentTabId === SearchApplicationContentTabs.INDICES && } + {contentTabId === SearchApplicationContentTabs.SCHEMA && } + {addIndicesFlyoutOpen && } + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx index 48dce9f524164..7b93214e0af8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/engines/engines_list.tsx @@ -166,7 +166,7 @@ export const EnginesList: React.FC = ({ createEngineFlyoutOpen }) => description: ( + +

{label}

+ + } + > {textarea}
) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx index 2ef04c8e2c18b..2c40eb0beafa4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_form.tsx @@ -17,22 +17,19 @@ import { EuiForm, EuiFormRow, EuiPanel, + EuiSpacer, EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Status } from '../../../../../../common/types/api'; -import { DependencyLookup, DisplayType } from '../../../../../../common/types/connectors'; +import { DisplayType } from '../../../../../../common/types/connectors'; import { ConnectorConfigurationApiLogic } from '../../../api/connector/update_connector_configuration_api_logic'; import { ConnectorConfigurationField } from './connector_configuration_field'; -import { - ConfigEntry, - ConnectorConfigurationLogic, - dependenciesSatisfied, -} from './connector_configuration_logic'; +import { ConnectorConfigurationLogic } from './connector_configuration_logic'; export const ConnectorConfigurationForm = () => { const { status } = useValues(ConnectorConfigurationApiLogic); @@ -40,14 +37,6 @@ export const ConnectorConfigurationForm = () => { const { localConfigView } = useValues(ConnectorConfigurationLogic); const { saveConfig, setIsEditing } = useActions(ConnectorConfigurationLogic); - const dependencyLookup: DependencyLookup = localConfigView.reduce( - (prev: Record, configEntry: ConfigEntry) => ({ - ...prev, - [configEntry.key]: configEntry.value, - }), - {} - ); - return ( { @@ -56,17 +45,16 @@ export const ConnectorConfigurationForm = () => { }} component="form" > - {localConfigView.map((configEntry) => { + {localConfigView.map((configEntry, index) => { const { default_value: defaultValue, depends_on: dependencies, key, display, label, + sensitive, tooltip, } = configEntry; - // toggle label goes next to the element, not in the row - const hasDependencies = dependencies.length > 0; const helpText = defaultValue ? i18n.translate( 'xpack.enterpriseSearch.content.indices.configurationConnector.config.defaultValue', @@ -76,26 +64,41 @@ export const ConnectorConfigurationForm = () => { } ) : ''; + // toggle and sensitive textarea labels go next to the element, not in the row const rowLabel = - display !== DisplayType.TOGGLE ? ( + display === DisplayType.TOGGLE || (display === DisplayType.TEXTAREA && sensitive) ? ( + <> + ) : (

{label}

- ) : ( - <> ); - return hasDependencies ? ( - dependenciesSatisfied(dependencies, dependencyLookup) ? ( - - - - - - ) : ( - <> - ) - ) : ( + if (dependencies.length > 0) { + // dynamic spacing without CSS + const previousField = localConfigView[index - 1]; + const nextField = localConfigView[index + 1]; + + const topSpacing = + !previousField || previousField.depends_on.length <= 0 ? : <>; + + const bottomSpacing = + !nextField || nextField.depends_on.length <= 0 ? : <>; + + return ( + <> + {topSpacing} + + + + + + {bottomSpacing} + + ); + } + + return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts index 9ff1fc60db168..f87d73b882ecd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.test.ts @@ -61,6 +61,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'oldBar', }, }, @@ -79,6 +80,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'oldBar', }, }, @@ -94,6 +96,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'oldBar', }, ], @@ -111,6 +114,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }); @@ -127,6 +131,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, }, @@ -142,13 +147,14 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, ], }); }); describe('setLocalConfigEntry', () => { - it('should set local config entry and sort keys', () => { + it('should set local config entry, and sort and filter keys', () => { ConnectorConfigurationLogic.actions.setConfigState({ bar: { default_value: '', @@ -160,6 +166,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, password: { @@ -172,8 +179,80 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigState({ bar: { @@ -186,6 +265,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, password: { @@ -198,8 +278,80 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }); ConnectorConfigurationLogic.actions.setLocalConfigEntry({ default_value: '', @@ -212,6 +364,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fafa', }); expect(ConnectorConfigurationLogic.values).toEqual({ @@ -227,6 +380,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, password: { @@ -239,8 +393,80 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, configView: [ { @@ -254,6 +480,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'foofoo', }, { @@ -267,8 +494,40 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + key: 'shownDependent1', + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'shownDependent2', + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, ], localConfigState: { bar: { @@ -281,6 +540,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fafa', }, password: { @@ -293,8 +553,80 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, + restricted: { + default_value: '', + depends_on: [], + display: DisplayType.TEXTBOX, + label: 'Restricted', + options: [], + order: 3, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: ['advanced'], + value: 'I am restricted', + }, + shownDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'foofoo' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 4, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (one dependency)', + }, + shownDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'foofoo' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 1', + options: [], + order: 5, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should appear (multiple dependencies)', + }, + hiddenDependent1: { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + hiddenDependent2: { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, }, localConfigView: [ { @@ -308,6 +640,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'fafa', }, { @@ -321,8 +654,40 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'fourthBar', }, + { + default_value: '', + depends_on: [{ field: 'bar', value: 'fafa' }], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent1', + label: 'Shown Dependent 2', + options: [], + order: 6, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (one dependency)', + }, + { + default_value: '', + depends_on: [ + { field: 'bar', value: 'fafa' }, + { field: 'password', value: 'fourthBar' }, + ], + display: DisplayType.TEXTBOX, + key: 'hiddenDependent2', + label: 'Shown Dependent', + options: [], + order: 7, + required: false, + sensitive: true, + tooltip: '', + ui_restrictions: [], + value: 'I should hide (multiple dependencies)', + }, ], }); }); @@ -345,6 +710,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, ], @@ -381,6 +747,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, ], @@ -402,6 +769,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: false, tooltip: '', + ui_restrictions: [], value: 'barbar', }, ], @@ -424,6 +792,7 @@ describe('ConnectorConfigurationLogic', () => { required: false, sensitive: true, tooltip: '', + ui_restrictions: [], value: 'Barbara', }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts index 02ebb7888b7b8..861ab90079229 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/connector_configuration_logic.ts @@ -66,17 +66,23 @@ export interface ConfigEntry { required: boolean; sensitive: boolean; tooltip: string; + ui_restrictions: string[]; value: string | number | boolean | null; } /** * - * Sorts the connector configuration by specified order (if present) + * Sorts and filters the connector configuration + * + * Sorting is done by specified order (if present) * otherwise by alphabetic order of keys * + * Filtering is done on any fields with ui_restrictions + * or that have not had their dependencies met + * */ -function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { - return Object.keys(config) +function sortAndFilterConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry[] { + const sortedConfig = Object.keys(config) .map( (key) => ({ @@ -97,6 +103,20 @@ function sortConnectorConfiguration(config: ConnectorConfiguration): ConfigEntry } return a.key.localeCompare(b.key); }); + + const dependencyLookup: DependencyLookup = sortedConfig.reduce( + (prev: Record, configEntry: ConfigEntry) => ({ + ...prev, + [configEntry.key]: configEntry.value, + }), + {} + ); + + return sortedConfig.filter( + (configEntry) => + configEntry.ui_restrictions.length <= 0 && + dependenciesSatisfied(configEntry.depends_on, dependencyLookup) + ); } export function ensureStringType(value: string | number | boolean | null): string { @@ -245,6 +265,8 @@ export const ConnectorConfigurationLogic = kea< required, sensitive, tooltip, + // eslint-disable-next-line @typescript-eslint/naming-convention + ui_restrictions, value, } ) => ({ @@ -259,6 +281,7 @@ export const ConnectorConfigurationLogic = kea< required, sensitive, tooltip, + ui_restrictions, value, }, }), @@ -276,11 +299,11 @@ export const ConnectorConfigurationLogic = kea< selectors: ({ selectors }) => ({ configView: [ () => [selectors.configState], - (configState: ConnectorConfiguration) => sortConnectorConfiguration(configState), + (configState: ConnectorConfiguration) => sortAndFilterConnectorConfiguration(configState), ], localConfigView: [ () => [selectors.localConfigState], - (configState) => sortConnectorConfiguration(configState), + (configState) => sortAndFilterConnectorConfiguration(configState), ], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx index a1bbe6c39956f..840010d3aa219 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings.tsx @@ -12,6 +12,8 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiLink, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + import { docLinks } from '../../../shared/doc_links'; import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; @@ -36,6 +38,21 @@ export const Settings: React.FC = () => { }), ]} pageHeader={{ + description: ( + + {i18n.translate('xpack.enterpriseSearch.content.settings.ingestLink', { + defaultMessage: 'ingest pipelines', + })} + + ), + }} + /> + ), pageTitle: i18n.translate('xpack.enterpriseSearch.content.settings.headerTitle', { defaultMessage: 'Content Settings', }), @@ -69,19 +86,12 @@ export const Settings: React.FC = () => { 'xpack.enterpriseSearch.content.settings.contentExtraction.description', { defaultMessage: - 'Allow all ingestion mechanisms on your Enterprise Search deployment to extract searchable content from binary files, like PDFs and Word documents. This setting applies to all new Elasticsearch indices created by an Enterprise Search ingestion mechanism.', + 'Extract searchable content from binary files, like PDFs and Word documents.', } )} label={i18n.translate('xpack.enterpriseSearch.content.settings.contactExtraction.label', { defaultMessage: 'Content extraction', })} - link={ - - {i18n.translate('xpack.enterpriseSearch.content.settings.contactExtraction.link', { - defaultMessage: 'Learn more about content extraction', - })} - - } onChange={() => setPipeline({ ...pipelineState, @@ -105,13 +115,6 @@ export const Settings: React.FC = () => { label={i18n.translate('xpack.enterpriseSearch.content.settings.whitespaceReduction.label', { defaultMessage: 'Whitespace reduction', })} - link={ - - {i18n.translate('xpack.enterpriseSearch.content.settings.whitespaceReduction.link', { - defaultMessage: 'Learn more about whitespace reduction', - })} - - } onChange={() => setPipeline({ ...pipelineState, @@ -139,9 +142,9 @@ export const Settings: React.FC = () => { defaultMessage: 'ML Inference', })} link={ - + {i18n.translate('xpack.enterpriseSearch.content.settings.mlInference.link', { - defaultMessage: 'Learn more about content extraction', + defaultMessage: 'Learn more about document enrichment with ML', })} } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx index fc0f3cca3e06c..673861d80e6ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/settings/settings_panel.tsx @@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n'; interface SettingsPanelProps { description: string; label: string; - link: React.ReactNode; + link?: React.ReactNode; onChange: (event: EuiSwitchEvent) => void; title: string; value: boolean; @@ -61,7 +61,7 @@ export const SettingsPanel: React.FC = ({ - {link} + {link && {link}} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts index ea5672222014f..7b5bc7bf286f5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/routes.ts @@ -29,8 +29,7 @@ export const ENGINES_PATH = `${ROOT_PATH}engines`; export enum EngineViewTabs { PREVIEW = 'preview', - INDICES = 'indices', - SCHEMA = 'schema', + CONTENT = 'content', CONNECT = 'connect', } export const ENGINE_CREATION_PATH = `${ENGINES_PATH}/new`; @@ -40,5 +39,10 @@ export const SEARCH_APPLICATION_CONNECT_PATH = `${ENGINE_PATH}/${EngineViewTabs. export enum SearchApplicationConnectTabs { API = 'api', } +export const SEARCH_APPLICATION_CONTENT_PATH = `${ENGINE_PATH}/${EngineViewTabs.CONTENT}/:contentTabId`; +export enum SearchApplicationContentTabs { + INDICES = 'indices', + SCHEMA = 'schema', +} export const ML_MANAGE_TRAINED_MODELS_PATH = '/app/ml/trained_models'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 938aaa88d1bdf..cb6663eebf6ab 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -92,6 +92,7 @@ class DocLinks { public languageClients: string; public licenseManagement: string; public machineLearningStart: string; + public mlDocumentEnrichment: string; public pluginsIngestAttachment: string; public queryDsl: string; public searchUIAppSearch: string; @@ -219,6 +220,7 @@ class DocLinks { this.languageClients = ''; this.licenseManagement = ''; this.machineLearningStart = ''; + this.mlDocumentEnrichment = ''; this.pluginsIngestAttachment = ''; this.queryDsl = ''; this.searchUIAppSearch = ''; @@ -340,6 +342,7 @@ class DocLinks { this.languageClients = docLinks.links.enterpriseSearch.languageClients; this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement; this.machineLearningStart = docLinks.links.enterpriseSearch.machineLearningStart; + this.mlDocumentEnrichment = docLinks.links.enterpriseSearch.mlDocumentEnrichment; this.pluginsIngestAttachment = docLinks.links.plugins.ingestAttachment; this.queryDsl = docLinks.links.query.queryDsl; this.searchUIAppSearch = docLinks.links.searchUI.appSearch; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 11458565f781a..dcb343a2beec4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -290,14 +290,9 @@ describe('useEnterpriseSearchEngineNav', () => { name: 'Preview', }, { - href: `/app/enterprise_search/content/engines/${engineName}/indices`, - id: 'enterpriseSearchEngineIndices', - name: 'Indices', - }, - { - href: `/app/enterprise_search/content/engines/${engineName}/schema`, - id: 'enterpriseSearchEngineSchema', - name: 'Schema', + href: `/app/enterprise_search/content/engines/${engineName}/content`, + id: 'enterpriseSearchApplicationsContent', + name: 'Content', }, { href: `/app/enterprise_search/content/engines/${engineName}/connect`, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index f06e32b9e30c8..7fbc354ff725f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -204,23 +204,14 @@ export const useEnterpriseSearchEngineNav = (engineName?: string, isEmptyState?: }), }, { - id: 'enterpriseSearchEngineIndices', - name: i18n.translate('xpack.enterpriseSearch.nav.engine.indicesTitle', { - defaultMessage: 'Indices', + id: 'enterpriseSearchApplicationsContent', + name: i18n.translate('xpack.enterpriseSearch.nav.engine.contentTitle', { + defaultMessage: 'Content', }), ...generateNavLink({ shouldNotCreateHref: true, - to: `${enginePath}/${EngineViewTabs.INDICES}`, - }), - }, - { - id: 'enterpriseSearchEngineSchema', - name: i18n.translate('xpack.enterpriseSearch.nav.engine.schemaTitle', { - defaultMessage: 'Schema', - }), - ...generateNavLink({ - shouldNotCreateHref: true, - to: `${enginePath}/${EngineViewTabs.SCHEMA}`, + shouldShowActiveForSubroutes: true, + to: `${enginePath}/${EngineViewTabs.CONTENT}`, }), }, { diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index f50a1c0ef3321..e802567b2ab7f 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -398,6 +398,9 @@ } } }, + "revision": { + "type": "long" + }, "rule_type_id": { "type": "keyword", "ignore_above": 1024 diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 6706db9635397..8621df23020bd 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -178,6 +178,7 @@ export const EventSchema = schema.maybe( ), }) ), + revision: ecsStringOrNumber(), rule_type_id: ecsString(), }) ), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index 7081c321cc659..768988aa3c07b 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -177,6 +177,9 @@ exports.EcsCustomPropertyMappings = { }, }, }, + revision: { + type: 'long', + }, rule_type_id: { type: 'keyword', ignore_above: 1024, diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index a1d73b452cf72..2635dbc05399f 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -16,6 +16,7 @@ export const FLEET_ENDPOINT_PACKAGE = 'endpoint'; export const FLEET_APM_PACKAGE = 'apm'; export const FLEET_SYNTHETICS_PACKAGE = 'synthetics'; export const FLEET_KUBERNETES_PACKAGE = 'kubernetes'; +export const FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE = 'profiler_symbolizer'; export const FLEET_CLOUD_SECURITY_POSTURE_PACKAGE = 'cloud_security_posture'; export const FLEET_CLOUD_SECURITY_POSTURE_KSPM_POLICY_TEMPLATE = 'kspm'; diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts index 1f5ea87c6d6f9..f093b20eaaeb4 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.test.ts @@ -13,6 +13,7 @@ import type { DataStreamMeta } from './package_policies_to_agent_permissions'; import { getDataStreamPrivileges, storedPackagePoliciesToAgentPermissions, + UNIVERSAL_PROFILING_PERMISSIONS, } from './package_policies_to_agent_permissions'; const packageInfoCache = new Map(); @@ -137,6 +138,56 @@ packageInfoCache.set('osquery_manager-0.3.0', { }, }, }); +packageInfoCache.set('profiler_symbolizer-8.8.0-preview', { + format_version: '2.7.0', + name: 'profiler_symbolizer', + title: 'Universal Profiling Symbolizer', + version: '8.8.0-preview', + license: 'basic', + description: + ' Fleet-wide, whole-system, continuous profiling with zero instrumentation. Symbolize native frames.', + type: 'integration', + release: 'beta', + categories: ['monitoring', 'elastic_stack'], + icons: [ + { + src: '/img/logo_profiling_symbolizer.svg', + title: 'logo symbolizer', + size: '32x32', + type: 'image/svg+xml', + }, + ], + owner: { github: 'elastic/profiling' }, + data_streams: [], + latestVersion: '8.8.0-preview', + notice: undefined, + status: 'not_installed', + assets: { + kibana: { + csp_rule_template: [], + dashboard: [], + visualization: [], + search: [], + index_pattern: [], + map: [], + lens: [], + security_rule: [], + ml_module: [], + tag: [], + osquery_pack_asset: [], + osquery_saved_query: [], + }, + elasticsearch: { + component_template: [], + ingest_pipeline: [], + ilm_policy: [], + transform: [], + index_template: [], + data_stream_ilm_policy: [], + ml_model: [], + }, + }, +}); describe('storedPackagePoliciesToAgentPermissions()', () => { it('Returns `undefined` if there are no package policies', async () => { @@ -363,6 +414,47 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { }, }); }); + + it('Returns the Universal Profiling permissions for profiler_symbolizer package', async () => { + const packagePolicies: PackagePolicy[] = [ + { + id: 'package-policy-uuid-test-123', + name: 'test-policy', + namespace: '', + enabled: true, + package: { name: 'profiler_symbolizer', version: '8.8.0-preview', title: 'Test Package' }, + inputs: [ + { + type: 'pf-elastic-symbolizer', + enabled: true, + streams: [], + }, + ], + created_at: '', + updated_at: '', + created_by: '', + updated_by: '', + revision: 1, + policy_id: '', + }, + ]; + + const permissions = await storedPackagePoliciesToAgentPermissions( + packageInfoCache, + packagePolicies + ); + + expect(permissions).toMatchObject({ + 'package-policy-uuid-test-123': { + indices: [ + { + names: ['profiling-*'], + privileges: UNIVERSAL_PROFILING_PERMISSIONS, + }, + ], + }, + }); + }); }); describe('getDataStreamPrivileges()', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts index 02c44024421ce..f8cd73901e0d7 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/package_policies_to_agent_permissions.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE } from '../../../common/constants'; + import { getNormalizedDataStreams } from '../../../common/services'; import type { @@ -19,6 +21,16 @@ import { pkgToPkgKey } from '../epm/registry'; export const DEFAULT_CLUSTER_PERMISSIONS = ['monitor']; +export const UNIVERSAL_PROFILING_PERMISSIONS = [ + 'auto_configure', + 'read', + 'create_doc', + 'create', + 'write', + 'index', + 'view_index_metadata', +]; + export async function storedPackagePoliciesToAgentPermissions( packageInfoCache: Map, packagePolicies?: PackagePolicy[] @@ -42,6 +54,12 @@ export async function storedPackagePoliciesToAgentPermissions( const pkg = packageInfoCache.get(pkgToPkgKey(packagePolicy.package))!; + // Special handling for Universal Profiling packages, as it does not use data streams _only_, + // but also indices that do not adhere to the convention. + if (pkg.name === FLEET_UNIVERSAL_PROFILING_SYMBOLIZER_PACKAGE) { + return Promise.resolve(universalProfilingPermissions(packagePolicy.id)); + } + const dataStreams = getNormalizedDataStreams(pkg); if (!dataStreams || dataStreams.length === 0) { return [packagePolicy.name, undefined]; @@ -175,3 +193,18 @@ export function getDataStreamPrivileges(dataStream: DataStreamMeta, namespace: s privileges, }; } + +async function universalProfilingPermissions(packagePolicyId: string): Promise<[string, any]> { + const profilingIndexPattern = 'profiling-*'; + return [ + packagePolicyId, + { + indices: [ + { + names: [profilingIndexPattern], + privileges: UNIVERSAL_PROFILING_PERMISSIONS, + }, + ], + }, + ]; +} diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts index 934e4200cb31c..355c5925702f7 100644 --- a/x-pack/plugins/infra/common/http_api/index.ts +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -14,3 +14,9 @@ export * from './log_alerts'; export * from './snapshot_api'; export * from './host_details'; export * from './infra'; + +/** + * Exporting versioned APIs types + */ +export * from './latest'; +export * as inventoryViewsV1 from './inventory_views/v1'; diff --git a/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts b/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts index bcfeeafcee06f..80e5e501169d6 100644 --- a/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts +++ b/x-pack/plugins/infra/common/http_api/infra/get_infra_metrics.ts @@ -23,8 +23,9 @@ export const RangeRT = rt.type({ }); export const InfraAssetMetadataTypeRT = rt.keyof({ - 'host.os.name': null, 'cloud.provider': null, + 'host.ip': null, + 'host.os.name': null, }); export const InfraAssetMetricsRT = rt.type({ @@ -35,7 +36,7 @@ export const InfraAssetMetricsRT = rt.type({ export const InfraAssetMetadataRT = rt.type({ // keep the actual field name from the index mappings name: InfraAssetMetadataTypeRT, - value: rt.union([rt.string, rt.number, rt.null]), + value: rt.union([rt.string, rt.null]), }); export const GetInfraMetricsRequestBodyPayloadRT = rt.intersection([ @@ -64,6 +65,7 @@ export const GetInfraMetricsResponsePayloadRT = rt.type({ export type InfraAssetMetrics = rt.TypeOf; export type InfraAssetMetadata = rt.TypeOf; +export type InfraAssetMetadataType = rt.TypeOf; export type InfraAssetMetricType = rt.TypeOf; export type InfraAssetMetricsItem = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts new file mode 100644 index 0000000000000..c229170b8007b --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/common.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; +import { either } from 'fp-ts/Either'; + +export const INVENTORY_VIEW_URL = '/api/infra/inventory_views'; +export const INVENTORY_VIEW_URL_ENTITY = `${INVENTORY_VIEW_URL}/{inventoryViewId}`; +export const getInventoryViewUrl = (inventoryViewId?: string) => + [INVENTORY_VIEW_URL, inventoryViewId].filter(Boolean).join('/'); + +const inventoryViewIdRT = new rt.Type( + 'InventoryViewId', + rt.string.is, + (u, c) => + either.chain(rt.string.validate(u, c), (id) => { + return id === '0' + ? rt.failure(u, c, `The inventory view with id ${id} is not configurable.`) + : rt.success(id); + }), + String +); + +export const inventoryViewRequestParamsRT = rt.type({ + inventoryViewId: inventoryViewIdRT, +}); + +export type InventoryViewRequestParams = rt.TypeOf; + +export const inventoryViewRequestQueryRT = rt.partial({ + sourceId: rt.string, +}); + +export type InventoryViewRequestQuery = rt.TypeOf; + +const inventoryViewAttributesResponseRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, + }), + rt.UnknownRecord, +]); + +const inventoryViewResponseRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: inventoryViewAttributesResponseRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export const inventoryViewResponsePayloadRT = rt.type({ + data: inventoryViewResponseRT, +}); + +export type GetInventoryViewResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts new file mode 100644 index 0000000000000..99350daa358b0 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/create_inventory_view.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const createInventoryViewAttributesRequestPayloadRT = rt.intersection([ + rt.type({ + name: nonEmptyStringRt, + }), + rt.UnknownRecord, + rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })), +]); + +export type CreateInventoryViewAttributesRequestPayload = rt.TypeOf< + typeof createInventoryViewAttributesRequestPayloadRT +>; + +export const createInventoryViewRequestPayloadRT = rt.type({ + attributes: createInventoryViewAttributesRequestPayloadRT, +}); + +export type CreateInventoryViewRequestPayload = rt.TypeOf< + typeof createInventoryViewRequestPayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts new file mode 100644 index 0000000000000..24812ccb43585 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/find_inventory_view.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const findInventoryViewAttributesResponseRT = rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, +}); + +const findInventoryViewResponseRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: findInventoryViewAttributesResponseRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export const findInventoryViewResponsePayloadRT = rt.type({ + data: rt.array(findInventoryViewResponseRT), +}); + +export type FindInventoryViewResponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts new file mode 100644 index 0000000000000..3e862bdaa3388 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/get_inventory_view.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const getInventoryViewRequestParamsRT = rt.type({ + inventoryViewId: rt.string, +}); + +export type GetInventoryViewRequestParams = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/index.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/index.ts new file mode 100644 index 0000000000000..74f0d3b6962a1 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './common'; +export * from './get_inventory_view'; +export * from './find_inventory_view'; +export * from './create_inventory_view'; +export * from './update_inventory_view'; diff --git a/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts b/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts new file mode 100644 index 0000000000000..7a2d33ebd6138 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/inventory_views/v1/update_inventory_view.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const updateInventoryViewAttributesRequestPayloadRT = rt.intersection([ + rt.type({ + name: nonEmptyStringRt, + }), + rt.UnknownRecord, + rt.exact(rt.partial({ isDefault: rt.undefined, isStatic: rt.undefined })), +]); + +export type UpdateInventoryViewAttributesRequestPayload = rt.TypeOf< + typeof updateInventoryViewAttributesRequestPayloadRT +>; + +export const updateInventoryViewRequestPayloadRT = rt.type({ + attributes: updateInventoryViewAttributesRequestPayloadRT, +}); + +export type UpdateInventoryViewRequestPayload = rt.TypeOf< + typeof updateInventoryViewRequestPayloadRT +>; diff --git a/x-pack/plugins/infra/common/http_api/latest.ts b/x-pack/plugins/infra/common/http_api/latest.ts new file mode 100644 index 0000000000000..519da4a60dec1 --- /dev/null +++ b/x-pack/plugins/infra/common/http_api/latest.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './inventory_views/v1'; diff --git a/x-pack/plugins/infra/common/inventory_views/defaults.ts b/x-pack/plugins/infra/common/inventory_views/defaults.ts new file mode 100644 index 0000000000000..ae78000968995 --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/defaults.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'; +import type { NonEmptyString } from '@kbn/io-ts-utils'; +import type { InventoryViewAttributes } from './types'; + +export const staticInventoryViewId = '0'; + +export const staticInventoryViewAttributes: InventoryViewAttributes = { + name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', { + defaultMessage: 'Default view', + }) as NonEmptyString, + isDefault: false, + isStatic: true, + metric: { + type: 'cpu', + }, + groupBy: [], + nodeType: 'host', + view: 'map', + customOptions: [], + boundsOverride: { + max: 1, + min: 0, + }, + autoBounds: true, + accountId: '', + region: '', + customMetrics: [], + legend: { + palette: 'cool', + steps: 10, + reverseColors: false, + }, + source: 'default', + sort: { + by: 'name', + direction: 'desc', + }, + timelineOpen: false, + filterQuery: { + kind: 'kuery', + expression: '', + }, + time: Date.now(), + autoReload: false, +}; diff --git a/x-pack/plugins/infra/common/inventory_views/index.ts b/x-pack/plugins/infra/common/inventory_views/index.ts new file mode 100644 index 0000000000000..ae809a6c7c615 --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './defaults'; +export * from './types'; diff --git a/x-pack/plugins/infra/common/inventory_views/inventory_view.mock.ts b/x-pack/plugins/infra/common/inventory_views/inventory_view.mock.ts new file mode 100644 index 0000000000000..a8f5ef6ce181b --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/inventory_view.mock.ts @@ -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 { staticInventoryViewAttributes } from './defaults'; +import type { InventoryView, InventoryViewAttributes } from './types'; + +export const createInventoryViewMock = ( + id: string, + attributes: InventoryViewAttributes, + updatedAt?: number, + version?: string +): InventoryView => ({ + id, + attributes: { + ...staticInventoryViewAttributes, + ...attributes, + }, + updatedAt, + version, +}); diff --git a/x-pack/plugins/infra/common/inventory_views/types.ts b/x-pack/plugins/infra/common/inventory_views/types.ts new file mode 100644 index 0000000000000..49979c1063efa --- /dev/null +++ b/x-pack/plugins/infra/common/inventory_views/types.ts @@ -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 { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const inventoryViewAttributesRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, + }), + rt.UnknownRecord, +]); + +export type InventoryViewAttributes = rt.TypeOf; + +export const inventoryViewRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: inventoryViewAttributesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export type InventoryView = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts b/x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts new file mode 100644 index 0000000000000..88771d1a76fcb --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/defaults.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { NonEmptyString } from '@kbn/io-ts-utils'; +import type { MetricsExplorerViewAttributes } from './types'; + +export const staticMetricsExplorerViewId = 'static'; + +export const staticMetricsExplorerViewAttributes: MetricsExplorerViewAttributes = { + name: i18n.translate('xpack.infra.savedView.defaultViewNameHosts', { + defaultMessage: 'Default view', + }) as NonEmptyString, + isDefault: false, + isStatic: true, + options: { + aggregation: 'avg', + metrics: [ + { + aggregation: 'avg', + field: 'system.cpu.total.norm.pct', + color: 'color0', + }, + { + aggregation: 'avg', + field: 'kubernetes.pod.cpu.usage.node.pct', + color: 'color1', + }, + { + aggregation: 'avg', + field: 'docker.cpu.total.pct', + color: 'color2', + }, + ], + source: 'default', + }, + chartOptions: { + type: 'line', + yAxisMode: 'fromZero', + stack: false, + }, + currentTimerange: { + from: 'now-1h', + to: 'now', + interval: '>=10s', + }, +}; diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/index.ts b/x-pack/plugins/infra/common/metrics_explorer_views/index.ts new file mode 100644 index 0000000000000..6cc0ccaa93a6d --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './types'; diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/metric_explorer_view.mock.ts b/x-pack/plugins/infra/common/metrics_explorer_views/metric_explorer_view.mock.ts new file mode 100644 index 0000000000000..e921c37dd21f8 --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/metric_explorer_view.mock.ts @@ -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 { staticMetricsExplorerViewAttributes } from './defaults'; +import type { MetricsExplorerView, MetricsExplorerViewAttributes } from './types'; + +export const createmetricsExplorerViewMock = ( + id: string, + attributes: MetricsExplorerViewAttributes, + updatedAt?: number, + version?: string +): MetricsExplorerView => ({ + id, + attributes: { + ...staticMetricsExplorerViewAttributes, + ...attributes, + }, + updatedAt, + version, +}); diff --git a/x-pack/plugins/infra/common/metrics_explorer_views/types.ts b/x-pack/plugins/infra/common/metrics_explorer_views/types.ts new file mode 100644 index 0000000000000..47ecb06ceace5 --- /dev/null +++ b/x-pack/plugins/infra/common/metrics_explorer_views/types.ts @@ -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 { nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const metricsExplorerViewAttributesRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + isDefault: rt.boolean, + isStatic: rt.boolean, + }), + rt.UnknownRecord, +]); + +export type MetricsExplorerViewAttributes = rt.TypeOf; + +export const metricsExplorerViewRT = rt.exact( + rt.intersection([ + rt.type({ + id: rt.string, + attributes: metricsExplorerViewAttributesRT, + }), + rt.partial({ + updatedAt: rt.number, + version: rt.string, + }), + ]) +); + +export type MetricsExplorerView = rt.TypeOf; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index f2bb22485fe9c..745a1f0169788 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -22,6 +22,9 @@ jest.mock('../../../hooks/use_kibana', () => ({ useKibanaContextForPlugin: () => ({ services: { ...mockStartServices, + charts: { + activeCursor: jest.fn(), + }, }, }), })); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index 8dd7762feb6b9..8b453579b5e67 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useRef } from 'react'; import { Axis, Chart, @@ -17,6 +17,7 @@ import { } from '@elastic/charts'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { useActiveCursor } from '@kbn/charts-plugin/public'; import { DataViewBase } from '@kbn/es-query'; import { first, last } from 'lodash'; @@ -66,7 +67,7 @@ export const ExpressionChart: React.FC = ({ timeRange, annotations, }) => { - const { uiSettings } = useKibanaContextForPlugin().services; + const { uiSettings, charts } = useKibanaContextForPlugin().services; const { isLoading, data } = useMetricsExplorerChartData( expression, @@ -77,6 +78,11 @@ export const ExpressionChart: React.FC = ({ timeRange ); + const chartRef = useRef(null); + const handleCursorUpdate = useActiveCursor(charts.activeCursor, chartRef, { + isDateHistogram: true, + }); + if (isLoading) { return ; } @@ -141,7 +147,7 @@ export const ExpressionChart: React.FC = ({ return ( <> - + = ({ tickFormat={createFormatterForMetric(metric)} domain={domain} /> - +
diff --git a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts index c9ce48c909f9a..6250d20750e29 100644 --- a/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/infra/public/hooks/use_lens_attributes.ts @@ -74,11 +74,7 @@ export const useLensAttributes = ({ return visualizationAttributes; }, [dataView, formulaAPI, options, type, visualizationType]); - const injectFilters = (data: { - timeRange: TimeRange; - filters: Filter[]; - query: Query; - }): LensAttributes | null => { + const injectFilters = (data: { filters: Filter[]; query: Query }): LensAttributes | null => { if (!attributes) { return null; } @@ -121,7 +117,7 @@ export const useLensAttributes = ({ return true; }, async execute(_context: ActionExecutionContext): Promise { - const injectedAttributes = injectFilters({ timeRange, filters, query }); + const injectedAttributes = injectFilters({ filters, query }); if (injectedAttributes) { navigateToPrefilledEditor( { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx new file mode 100644 index 0000000000000..bbddb338ef73f --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/chart_loader.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiProgress, EuiFlexItem, EuiLoadingChart, useEuiTheme } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiStyled } from '@kbn/kibana-react-plugin/common'; + +export const ChartLoader = ({ + children, + loading, + style, + loadedOnce = false, + hasTitle = false, +}: { + style?: React.CSSProperties; + children: React.ReactNode; + loadedOnce: boolean; + loading: boolean; + hasTitle?: boolean; +}) => { + const { euiTheme } = useEuiTheme(); + return ( + + {loading && ( + + )} + {loading && !loadedOnce ? ( + + + + + + ) : ( + children + )} + + ); +}; + +const LoaderContainer = euiStyled.div` + position: relative; + border-radius: ${({ theme }) => theme.eui.euiSizeS}; + overflow: hidden; + height: 100%; +`; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx index 9985db0751fd4..34e536aaf37d2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/lens_wrapper.tsx @@ -4,20 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiLoadingChart } from '@elastic/eui'; import { Filter, Query, TimeRange } from '@kbn/es-query'; import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; import { useIntersectedOnce } from '../../../../../hooks/use_intersection_once'; import { LensAttributes } from '../../../../../common/visualizations'; +import { ChartLoader } from './chart_loader'; -export interface Props { +export interface LensWrapperProps { id: string; attributes: LensAttributes | null; dateRange: TimeRange; @@ -26,7 +24,10 @@ export interface Props { extraActions: Action[]; lastReloadRequestTime?: number; style?: React.CSSProperties; + loading?: boolean; + hasTitle?: boolean; onBrushEnd?: (data: BrushTriggerEvent['data']) => void; + onLoad?: () => void; } export const LensWrapper = ({ @@ -39,12 +40,20 @@ export const LensWrapper = ({ style, onBrushEnd, lastReloadRequestTime, -}: Props) => { - const intersectionRef = React.useRef(null); + loading = false, + hasTitle = false, +}: LensWrapperProps) => { + const intersectionRef = useRef(null); + const [loadedOnce, setLoadedOnce] = useState(false); + + const [state, setState] = useState({ + attributes, + lastReloadRequestTime, + query, + filters, + dateRange, + }); - const [currentLastReloadRequestTime, setCurrentLastReloadRequestTime] = useState< - number | undefined - >(lastReloadRequestTime); const { services: { lens }, } = useKibanaContextForPlugin(); @@ -56,38 +65,57 @@ export const LensWrapper = ({ useEffect(() => { if ((intersection?.intersectionRatio ?? 0) === 1) { - setCurrentLastReloadRequestTime(lastReloadRequestTime); + setState({ + attributes, + lastReloadRequestTime, + query, + filters, + dateRange, + }); } - }, [intersection?.intersectionRatio, lastReloadRequestTime]); + }, [ + attributes, + dateRange, + filters, + intersection?.intersectionRatio, + lastReloadRequestTime, + query, + ]); - const isReady = attributes && intersectedOnce; + const isReady = state.attributes && intersectedOnce; return (
- {!isReady ? ( - - - - - - ) : ( - - )} + + {state.attributes && ( + { + if (!loadedOnce) { + setLoadedOnce(true); + } + }} + /> + )} +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx index 9df937983ae1e..a98de9c773c52 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/chart/metric_chart_wrapper.tsx @@ -4,47 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useEffect, useMemo, useRef } from 'react'; -import { - Chart, - Metric, - MetricTrendShape, - type MetricWNumber, - type MetricWTrend, -} from '@elastic/charts'; -import { EuiPanel } from '@elastic/eui'; +import React, { useEffect, useRef } from 'react'; +import { Chart, Metric, type MetricWNumber, type MetricWTrend } from '@elastic/charts'; +import { EuiPanel, EuiToolTip } from '@elastic/eui'; import styled from 'styled-components'; -import { EuiLoadingChart } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiToolTip } from '@elastic/eui'; -import { EuiProgress } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { useEuiTheme } from '@elastic/eui'; -import type { SnapshotNode, SnapshotNodeMetric } from '../../../../../../common/http_api'; -import { createInventoryMetricFormatter } from '../../../inventory_view/lib/create_inventory_metric_formatter'; -import type { SnapshotMetricType } from '../../../../../../common/inventory_models/types'; +import { ChartLoader } from './chart_loader'; -type MetricType = keyof Pick; - -type AcceptedType = SnapshotMetricType | 'hostsCount'; - -export interface ChartBaseProps - extends Pick< - MetricWTrend, - 'title' | 'color' | 'extra' | 'subtitle' | 'trendA11yDescription' | 'trendA11yTitle' - > { - type: AcceptedType; - toolTip: string; - metricType: MetricType; - ['data-test-subj']?: string; -} - -interface Props extends ChartBaseProps { +export interface Props extends Pick { id: string; - nodes: SnapshotNode[]; loading: boolean; - overrideValue?: number; + value: number; + toolTip: string; + ['data-test-subj']?: string; } const MIN_HEIGHT = 150; @@ -54,24 +25,13 @@ export const MetricChartWrapper = ({ extra, id, loading, - metricType, - nodes, - overrideValue, + value, subtitle, title, toolTip, - trendA11yDescription, - trendA11yTitle, - type, ...props }: Props) => { - const { euiTheme } = useEuiTheme(); const loadedOnce = useRef(false); - const metrics = useMemo(() => (nodes ?? [])[0]?.metrics ?? [], [nodes]); - const metricsTimeseries = useMemo( - () => (metrics ?? []).find((m) => m.name === type)?.timeseries, - [metrics, type] - ); useEffect(() => { if (!loadedOnce.current && !loading) { @@ -82,66 +42,29 @@ export const MetricChartWrapper = ({ }; }, [loading]); - const metricsValue = useMemo(() => { - if (overrideValue) { - return overrideValue; - } - return (metrics ?? []).find((m) => m.name === type)?.[metricType] ?? 0; - }, [metricType, metrics, overrideValue, type]); - const metricsData: MetricWNumber = { title, subtitle, color, extra, - value: metricsValue, - valueFormatter: (d: number) => - type === 'hostsCount' ? d.toString() : createInventoryMetricFormatter({ type })(d), - ...(!!metricsTimeseries - ? { - trend: metricsTimeseries.rows.map((row) => ({ x: row.timestamp, y: row.metric_0 ?? 0 })), - trendShape: MetricTrendShape.Area, - trendA11yTitle, - trendA11yDescription, - } - : {}), + value, + valueFormatter: (d: number) => d.toString(), }; return ( -
- {loading && ( - - )} - {loading && !loadedOnce.current ? ( - - - - - - ) : ( - - - - - - )} -
+ + + + + + +
); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx index 1c6320c142d7a..46392fa8609d1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/host_details_flyout/metadata/metadata.test.tsx @@ -32,42 +32,12 @@ const metadataProps: TabProps = { name: 'host-1', cloudProvider: 'gcp', }, - rx: { - name: 'rx', - value: 0, - max: 0, - avg: 0, - }, - tx: { - name: 'tx', - value: 0, - max: 0, - avg: 0, - }, - memory: { - name: 'memory', - value: 0.5445920331099282, - max: 0.5445920331099282, - avg: 0.5445920331099282, - }, - cpu: { - name: 'cpu', - value: 0.2000718443867342, - max: 0.2000718443867342, - avg: 0.2000718443867342, - }, - diskLatency: { - name: 'diskLatency', - value: null, - max: 0, - avg: 0, - }, - memoryTotal: { - name: 'memoryTotal', - value: 16777216, - max: 16777216, - avg: 16777216, - }, + rx: 0, + tx: 0, + memory: 0.5445920331099282, + cpu: 0.2000718443867342, + diskLatency: 0, + memoryTotal: 16777216, }, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx index e8e8a8a8e7c4f..d42944857af34 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_container.tsx @@ -10,12 +10,13 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { InfraLoadingPanel } from '../../../../components/loading'; import { useMetricsDataViewContext } from '../hooks/use_data_view'; -import { UnifiedSearchBar } from './unified_search_bar'; +import { UnifiedSearchBar } from './search_bar/unified_search_bar'; import { HostsTable } from './hosts_table'; -import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { KPIGrid } from './kpis/kpi_grid'; import { Tabs } from './tabs/tabs'; import { AlertsQueryProvider } from '../hooks/use_alerts_query'; -import { KPIGrid } from './kpis/kpi_grid'; +import { HostsViewProvider } from '../hooks/use_hosts_view'; +import { HostsTableProvider } from '../hooks/use_hosts_table'; export const HostContainer = () => { const { dataView, loading, hasError } = useMetricsDataViewContext(); @@ -38,19 +39,21 @@ export const HostContainer = () => { - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx index ca6f904ceea84..535afe8befff5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/hosts_table.tsx @@ -5,93 +5,78 @@ * 2.0. */ -import React, { useCallback } from 'react'; -import { EuiInMemoryTable } from '@elastic/eui'; +import React from 'react'; +import { EuiBasicTable } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; import { NoData } from '../../../../components/empty_states'; -import { InfraLoadingPanel } from '../../../../components/loading'; -import { useHostsTable } from '../hooks/use_hosts_table'; -import { useTableProperties } from '../hooks/use_table_properties_url_state'; +import { HostNodeRow, useHostsTableContext } from '../hooks/use_hosts_table'; import { useHostsViewContext } from '../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../hooks/use_unified_search'; import { Flyout } from './host_details_flyout/flyout'; +import { DEFAULT_PAGE_SIZE } from '../constants'; -export const HostsTable = () => { - const { hostNodes, loading } = useHostsViewContext(); - const { onSubmit, searchCriteria } = useUnifiedSearchContext(); - const [properties, setProperties] = useTableProperties(); - - const { columns, items, isFlyoutOpen, closeFlyout, clickedItem } = useHostsTable(hostNodes, { - time: searchCriteria.dateRange, - }); - - const noData = items.length === 0; - - const onTableChange = useCallback( - ({ page = {}, sort = {} }) => { - const { index: pageIndex, size: pageSize } = page; - const { field, direction } = sort; - - const sorting = field && direction ? { field, direction } : true; - const pagination = pageIndex >= 0 && pageSize !== 0 ? { pageIndex, pageSize } : true; - - if (!isEqual(properties.sorting, sorting)) { - setProperties({ sorting }); - } - if (!isEqual(properties.pagination, pagination)) { - setProperties({ pagination }); - } - }, - [setProperties, properties.pagination, properties.sorting] - ); +const PAGE_SIZE_OPTIONS = [5, 10, 20]; - if (loading) { - return ( - - ); - } +export const HostsTable = () => { + const { loading } = useHostsViewContext(); + const { onSubmit } = useUnifiedSearchContext(); - if (noData) { - return ( - onSubmit()} - testString="noMetricsDataPrompt" - /> - ); - } + const { + columns, + items, + currentPage, + isFlyoutOpen, + closeFlyout, + clickedItem, + onTableChange, + pagination, + sorting, + } = useHostsTableContext(); return ( <> - onSubmit()} + testString="noMetricsDataPrompt" + /> + ) + } /> {isFlyoutOpen && clickedItem && } diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx index 396c0bd72ad71..14a617682bf25 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/hosts_tile.tsx @@ -4,22 +4,46 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { i18n } from '@kbn/i18n'; import React from 'react'; +import { useHostCountContext } from '../../hooks/use_host_count'; +import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; -import { useHostsViewContext } from '../../hooks/use_hosts_view'; -import { type ChartBaseProps, MetricChartWrapper } from '../chart/metric_chart_wrapper'; +import { type Props, MetricChartWrapper } from '../chart/metric_chart_wrapper'; -export const HostsTile = ({ type, ...props }: ChartBaseProps) => { - const { hostNodes, loading } = useHostsViewContext(); +const HOSTS_CHART: Omit = { + id: `metric-hostCount`, + color: '#6DCCB1', + title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', { + defaultMessage: 'Hosts', + }), + toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', { + defaultMessage: 'The number of hosts returned by your current search criteria.', + }), + ['data-test-subj']: 'hostsView-metricsTrend-hosts', +}; + +export const HostsTile = () => { + const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); + const { searchCriteria } = useUnifiedSearchContext(); + + const getSubtitle = () => { + return searchCriteria.limit < (hostCountData?.count.value ?? 0) + ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.hostCount.limit', { + defaultMessage: 'Limited to {limit}', + values: { + limit: searchCriteria.limit, + }, + }) + : undefined; + }; return ( ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx index 968e7462b38f4..c3f751d26befb 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/kpi_grid.tsx @@ -6,16 +6,13 @@ */ import React from 'react'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; - +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { KPIChartProps, Tile } from './tile'; +import { HostCountProvider } from '../../hooks/use_host_count'; import { HostsTile } from './hosts_tile'; -import { ChartBaseProps } from '../chart/metric_chart_wrapper'; -const KPI_CHARTS: KPIChartProps[] = [ +const KPI_CHARTS: Array> = [ { type: 'cpu', trendLine: true, @@ -23,9 +20,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.title', { defaultMessage: 'CPU usage', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.cpu.tooltip', { defaultMessage: 'Average of percentage of CPU time spent in states other than Idle and IOWait, normalized by the number of CPU cores. Includes both time spent on user space and kernel space. 100% means all CPUs of the host are busy.', @@ -38,9 +32,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.title', { defaultMessage: 'Memory usage', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.memory.tooltip', { defaultMessage: "Average of percentage of main memory usage excluding page cache. This includes resident memory for all processes plus memory used by the kernel structures and code apart the page cache. A high level indicates a situation of memory saturation for a host. 100% means the main memory is entirely filled with memory that can't be reclaimed, except by swapping out.", @@ -53,9 +44,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.title', { defaultMessage: 'Network inbound (RX)', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.rx.tooltip', { defaultMessage: 'Number of bytes which have been received per second on the public interfaces of the hosts.', @@ -68,9 +56,6 @@ const KPI_CHARTS: KPIChartProps[] = [ title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.title', { defaultMessage: 'Network outbound (TX)', }), - subtitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.subtitle', { - defaultMessage: 'Average', - }), toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.tx.tooltip', { defaultMessage: 'Number of bytes which have been received per second on the public interfaces of the hosts.', @@ -78,38 +63,24 @@ const KPI_CHARTS: KPIChartProps[] = [ }, ]; -const HOSTS_CHART: ChartBaseProps = { - type: 'hostsCount', - color: '#6DCCB1', - metricType: 'value', - title: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.title', { - defaultMessage: 'Hosts', - }), - trendA11yTitle: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.a11y.title', { - defaultMessage: 'Hosts count.', - }), - toolTip: i18n.translate('xpack.infra.hostsViewPage.metricTrend.hostCount.tooltip', { - defaultMessage: 'The number of hosts returned by your current search criteria.', - }), - ['data-test-subj']: 'hostsView-metricsTrend-hosts', -}; - export const KPIGrid = () => { return ( - - - - - {KPI_CHARTS.map(({ ...chartProp }) => ( - - + + + + - ))} - + {KPI_CHARTS.map(({ ...chartProp }) => ( + + + + ))} + + ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx index 480e6c415dc45..89eebeefd240e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/kpis/tile.tsx @@ -4,23 +4,29 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; -import { Action } from '@kbn/ui-actions-plugin/public'; +import { i18n } from '@kbn/i18n'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiI18n, + EuiToolTip, +} from '@elastic/eui'; import styled from 'styled-components'; -import { EuiToolTip } from '@elastic/eui'; import { useLensAttributes } from '../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; import { HostsLensMetricChartFormulas } from '../../../../../common/visualizations'; import { useHostsViewContext } from '../../hooks/use_hosts_view'; import { LensWrapper } from '../chart/lens_wrapper'; +import { createHostsFilter } from '../../utils'; +import { useHostCountContext } from '../../hooks/use_host_count'; +import { useAfterLoadedState } from '../../hooks/use_after_loaded_state'; export interface KPIChartProps { title: string; @@ -35,7 +41,6 @@ const MIN_HEIGHT = 150; export const Tile = ({ title, - subtitle, type, backgroundColor, toolTip, @@ -43,14 +48,28 @@ export const Tile = ({ }: KPIChartProps) => { const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest } = useHostsViewContext(); + const { requestTs, hostNodes, loading: hostsLoading } = useHostsViewContext(); + const { data: hostCountData, isRequestRunning: hostCountLoading } = useHostCountContext(); + + const getSubtitle = () => { + return searchCriteria.limit < (hostCountData?.count.value ?? 0) + ? i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average.limit', { + defaultMessage: 'Average (of {limit} hosts)', + values: { + limit: searchCriteria.limit, + }, + }) + : i18n.translate('xpack.infra.hostsViewPage.metricTrend.subtitle.average', { + defaultMessage: 'Average', + }); + }; const { attributes, getExtraActions, error } = useLensAttributes({ type, dataView, options: { title, - subtitle, + subtitle: getSubtitle(), backgroundColor, showTrendLine: trendLine, showTitle: false, @@ -58,15 +77,24 @@ export const Tile = ({ visualizationType: 'metricChart', }); - const filters = [...searchCriteria.filters, ...searchCriteria.panelFilters]; + const hostsFilterQuery = useMemo(() => { + return createHostsFilter( + hostNodes.map((p) => p.name), + dataView + ); + }, [hostNodes, dataView]); + + const filters = useMemo( + () => [...searchCriteria.filters, ...searchCriteria.panelFilters, ...[hostsFilterQuery]], + [hostsFilterQuery, searchCriteria.filters, searchCriteria.panelFilters] + ); + const extraActionOptions = getExtraActions({ timeRange: searchCriteria.dateRange, filters, query: searchCriteria.query, }); - const extraActions: Action[] = [extraActionOptions.openInLens]; - const handleBrushEnd = ({ range }: BrushTriggerEvent['data']) => { const [min, max] = range; onSubmit({ @@ -78,6 +106,14 @@ export const Tile = ({ }); }; + const loading = hostsLoading || !attributes || hostCountLoading; + const { afterLoadedState } = useAfterLoadedState(loading, { + attributes, + lastReloadRequestTime: requestTs, + ...searchCriteria, + filters, + }); + return ( )} @@ -131,7 +168,7 @@ export const Tile = ({ const EuiPanelStyled = styled(EuiPanel)` .echMetric { - border-radius: ${(p) => p.theme.eui.euiBorderRadius}; + border-radius: ${({ theme }) => theme.eui.euiBorderRadius}; pointer-events: none; } `; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx similarity index 79% rename from x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx rename to x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx index a3e82b9901422..e2bd7d0c74dae 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/controls_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/controls_content.tsx @@ -12,15 +12,16 @@ import { type ControlGroupInput, } from '@kbn/controls-plugin/public'; import { ViewMode } from '@kbn/embeddable-plugin/public'; -import type { Filter, Query, TimeRange } from '@kbn/es-query'; +import { compareFilters, COMPARE_ALL_OPTIONS, Filter, Query, TimeRange } from '@kbn/es-query'; import { DataView } from '@kbn/data-views-plugin/public'; -import { Subscription } from 'rxjs'; -import { useControlPanels } from '../hooks/use_control_panels_url_state'; +import { skipWhile, Subscription } from 'rxjs'; +import { useControlPanels } from '../../hooks/use_control_panels_url_state'; interface Props { dataView: DataView | undefined; timeRange: TimeRange; filters: Filter[]; + selectedOptions: Filter[]; query: Query; onFiltersChange: (filters: Filter[]) => void; } @@ -29,6 +30,7 @@ export const ControlsContent: React.FC = ({ dataView, filters, query, + selectedOptions, timeRange, onFiltersChange, }) => { @@ -55,15 +57,21 @@ export const ControlsContent: React.FC = ({ const loadCompleteHandler = useCallback( (controlGroup: ControlGroupAPI) => { if (!controlGroup) return; - inputSubscription.current = controlGroup.onFiltersPublished$.subscribe((newFilters) => { - onFiltersChange(newFilters); - }); + inputSubscription.current = controlGroup.onFiltersPublished$ + .pipe( + skipWhile((newFilters) => + compareFilters(selectedOptions, newFilters, COMPARE_ALL_OPTIONS) + ) + ) + .subscribe((newFilters) => { + onFiltersChange(newFilters); + }); filterSubscription.current = controlGroup .getInput$() .subscribe(({ panels }) => setControlPanels(panels)); }, - [onFiltersChange, setControlPanels] + [onFiltersChange, setControlPanels, selectedOptions] ); useEffect(() => { diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx new file mode 100644 index 0000000000000..1d8ce4f9c9e87 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/limit_options.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + EuiButtonGroup, + EuiButtonGroupOptionProps, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiToolTip, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { HOST_LIMIT_OPTIONS } from '../../constants'; +import { HostLimitOptions } from '../../types'; + +interface Props { + limit: HostLimitOptions; + onChange: (limit: number) => void; +} + +export const LimitOptions = ({ limit, onChange }: Props) => { + return ( + + + + + + + + + + + + + + + onChange(value)} + /> + + + ); +}; + +const buildId = (option: number) => `hostLimit_${option}`; +const options: EuiButtonGroupOptionProps[] = HOST_LIMIT_OPTIONS.map((option) => ({ + id: buildId(option), + label: `${option}`, + value: option, + 'data-test-subj': `hostsViewLimitSelection${option}button`, +})); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx new file mode 100644 index 0000000000000..ef515cc018839 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.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 React, { useMemo } from 'react'; +import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGrid, + useEuiTheme, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { METRICS_APP_DATA_TEST_SUBJ } from '../../../../../apps/metrics_app'; +import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; +import { useUnifiedSearchContext } from '../../hooks/use_unified_search'; +import { ControlsContent } from './controls_content'; +import { useMetricsDataViewContext } from '../../hooks/use_data_view'; +import { HostsSearchPayload } from '../../hooks/use_unified_search_url_state'; +import { LimitOptions } from './limit_options'; +import { HostLimitOptions } from '../../types'; + +export const UnifiedSearchBar = () => { + const { + services: { unifiedSearch, application }, + } = useKibanaContextForPlugin(); + const { dataView } = useMetricsDataViewContext(); + const { searchCriteria, onSubmit } = useUnifiedSearchContext(); + + const { SearchBar } = unifiedSearch.ui; + + const onLimitChange = (limit: number) => { + onSubmit({ limit }); + }; + + const onPanelFiltersChange = (panelFilters: Filter[]) => { + if (!compareFilters(searchCriteria.panelFilters, panelFilters, COMPARE_ALL_OPTIONS)) { + onSubmit({ panelFilters }); + } + }; + + const handleRefresh = (payload: HostsSearchPayload, isUpdate?: boolean) => { + // This makes sure `onQueryChange` is only called when the submit button is clicked + if (isUpdate === false) { + onSubmit(payload); + } + }; + + return ( + + + + 0.5)', + })} + onQuerySubmit={handleRefresh} + showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)} + showDatePicker + showFilterBar + showQueryInput + showQueryMenu + useDefaultBehaviors + /> + + + + + + + + + + + + + + + ); +}; + +const StickyContainer = (props: { children: React.ReactNode }) => { + const { euiTheme } = useEuiTheme(); + + const top = useMemo(() => { + const wrapper = document.querySelector(`[data-test-subj="${METRICS_APP_DATA_TEST_SUBJ}"]`); + if (!wrapper) { + return `calc(${euiTheme.size.xxxl} * 2)`; + } + + return `${wrapper.getBoundingClientRect().top}px`; + }, [euiTheme]); + + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx index 0fad370960f22..6813dee1caa10 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx @@ -9,7 +9,6 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { InfraLoadingPanel } from '../../../../../../components/loading'; -import { SnapshotNode } from '../../../../../../../common/http_api'; import { LogStream } from '../../../../../../components/log_stream'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; @@ -24,10 +23,13 @@ export const LogsTabContent = () => { const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]); const { hostNodes, loading } = useHostsViewContext(); - const hostsFilterQuery = useMemo(() => createHostsFilter(hostNodes), [hostNodes]); + const hostsFilterQuery = useMemo( + () => createHostsFilter(hostNodes.map((p) => p.name)), + [hostNodes] + ); const logsLinkToStreamQuery = useMemo(() => { - const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes); + const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes.map((p) => p.name)); if (filterQuery.query && hostsFilterQueryParam) { return `${filterQuery.query} and ${hostsFilterQueryParam}`; @@ -80,12 +82,12 @@ export const LogsTabContent = () => { ); }; -const createHostsFilterQueryParam = (hostNodes: SnapshotNode[]): string => { +const createHostsFilterQueryParam = (hostNodes: string[]): string => { if (!hostNodes.length) { return ''; } - const joinedHosts = hostNodes.map((p) => p.name).join(' or '); + const joinedHosts = hostNodes.join(' or '); const hostsQueryParam = `host.name:(${joinedHosts})`; return hostsQueryParam; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx index 252bea5389e3a..f81228957107a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metric_chart.tsx @@ -4,20 +4,28 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { Action } from '@kbn/ui-actions-plugin/public'; import { BrushTriggerEvent } from '@kbn/charts-plugin/public'; -import { EuiIcon, EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; -import { EuiI18n } from '@elastic/eui'; +import { + EuiIcon, + EuiPanel, + EuiI18n, + EuiFlexGroup, + EuiFlexItem, + EuiText, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; import { useLensAttributes } from '../../../../../../hooks/use_lens_attributes'; import { useMetricsDataViewContext } from '../../../hooks/use_data_view'; import { useUnifiedSearchContext } from '../../../hooks/use_unified_search'; import { HostsLensLineChartFormulas } from '../../../../../../common/visualizations'; import { useHostsViewContext } from '../../../hooks/use_hosts_view'; +import { createHostsFilter } from '../../../utils'; +import { useHostsTableContext } from '../../../hooks/use_hosts_table'; import { LensWrapper } from '../../chart/lens_wrapper'; +import { useAfterLoadedState } from '../../../hooks/use_after_loaded_state'; export interface MetricChartProps { title: string; @@ -29,9 +37,18 @@ export interface MetricChartProps { const MIN_HEIGHT = 300; export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => { + const { euiTheme } = useEuiTheme(); const { searchCriteria, onSubmit } = useUnifiedSearchContext(); const { dataView } = useMetricsDataViewContext(); - const { baseRequest } = useHostsViewContext(); + const { requestTs, loading } = useHostsViewContext(); + const { currentPage } = useHostsTableContext(); + + // prevents updates on requestTs and serchCriteria states from relaoding the chart + // we want it to reload only once the table has finished loading + const { afterLoadedState } = useAfterLoadedState(loading, { + lastReloadRequestTime: requestTs, + ...searchCriteria, + }); const { attributes, getExtraActions, error } = useLensAttributes({ type, @@ -43,11 +60,22 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => visualizationType: 'lineChart', }); - const filters = [...searchCriteria.filters, ...searchCriteria.panelFilters]; + const hostsFilterQuery = useMemo(() => { + return createHostsFilter( + currentPage.map((p) => p.name), + dataView + ); + }, [currentPage, dataView]); + + const filters = [ + ...afterLoadedState.filters, + ...afterLoadedState.panelFilters, + ...[hostsFilterQuery], + ]; const extraActionOptions = getExtraActions({ - timeRange: searchCriteria.dateRange, + timeRange: afterLoadedState.dateRange, filters, - query: searchCriteria.query, + query: afterLoadedState.query, }); const extraActions: Action[] = [extraActionOptions.openInLens]; @@ -69,12 +97,15 @@ export const MetricChart = ({ title, type, breakdownSize }: MetricChartProps) => hasShadow={false} hasBorder paddingSize={error ? 'm' : 'none'} - style={{ minHeight: MIN_HEIGHT }} + css={css` + min-height: calc(${MIN_HEIGHT} + ${euiTheme.size.l}); + position: 'relative'; + `} data-test-subj={`hostsView-metricChart-${type}`} > {error ? ( attributes={attributes} style={{ height: MIN_HEIGHT }} extraActions={extraActions} - lastReloadRequestTime={baseRequest.requestTs} - dateRange={searchCriteria.dateRange} + lastReloadRequestTime={afterLoadedState.lastReloadRequestTime} + dateRange={afterLoadedState.dateRange} filters={filters} - query={searchCriteria.query} + query={afterLoadedState.query} onBrushEnd={handleBrushEnd} + loading={loading} + hasTitle /> )} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx index e307dde0d09e5..7f3dac7a3af16 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/metrics/metrics_grid.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiFlexGroup, EuiText, EuiI18n } from '@elastic/eui'; +import { EuiFlexGrid, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { MetricChart, MetricChartProps } from './metric_chart'; @@ -64,32 +64,12 @@ const CHARTS_IN_ORDER: Array & { fullRo export const MetricsGrid = React.memo(() => { return ( - - - - - - {DEFAULT_BREAKDOWN_SIZE}, - attribute: name, - }} - /> - - - - - - - {CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => ( - - - - ))} - - - + + {CHARTS_IN_ORDER.map(({ fullRow, ...chartProp }) => ( + + + + ))} + ); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx deleted file mode 100644 index 168f825a9d2d1..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/unified_search_bar.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo } from 'react'; -import { compareFilters, COMPARE_ALL_OPTIONS, type Filter } from '@kbn/es-query'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGrid, useEuiTheme } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { EuiHorizontalRule } from '@elastic/eui'; -import { METRICS_APP_DATA_TEST_SUBJ } from '../../../../apps/metrics_app'; -import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; -import { useUnifiedSearchContext } from '../hooks/use_unified_search'; -import { ControlsContent } from './controls_content'; -import { useMetricsDataViewContext } from '../hooks/use_data_view'; -import { HostsSearchPayload } from '../hooks/use_unified_search_url_state'; - -export const UnifiedSearchBar = () => { - const { - services: { unifiedSearch, application }, - } = useKibanaContextForPlugin(); - const { dataView } = useMetricsDataViewContext(); - const { searchCriteria, onSubmit } = useUnifiedSearchContext(); - - const { SearchBar } = unifiedSearch.ui; - - const onPanelFiltersChange = (panelFilters: Filter[]) => { - if (!compareFilters(searchCriteria.panelFilters, panelFilters, COMPARE_ALL_OPTIONS)) { - onSubmit({ panelFilters }); - } - }; - - const handleRefresh = (payload: HostsSearchPayload, isUpdate?: boolean) => { - // This makes sure `onQueryChange` is only called when the submit button is clicked - if (isUpdate === false) { - onSubmit(payload); - } - }; - - return ( - - 0.5)', - })} - onQuerySubmit={handleRefresh} - showSaveQuery={Boolean(application?.capabilities?.visualize?.saveQuery)} - showDatePicker - showFilterBar - showQueryInput - showQueryMenu - useDefaultBehaviors - /> - - - - ); -}; - -const StickyContainer = (props: { children: React.ReactNode }) => { - const { euiTheme } = useEuiTheme(); - - const top = useMemo(() => { - const wrapper = document.querySelector(`[data-test-subj="${METRICS_APP_DATA_TEST_SUBJ}"]`); - if (!wrapper) { - return `calc(${euiTheme.size.xxxl} * 2)`; - } - - return `${wrapper.getBoundingClientRect().top}px`; - }, [euiTheme]); - - return ( - - ); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts index 98aa8a145e3a0..69cfc446d0095 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/constants.ts @@ -7,12 +7,17 @@ import { i18n } from '@kbn/i18n'; import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; -import { AlertStatusFilter } from './types'; +import { AlertStatusFilter, HostLimitOptions } from './types'; export const ALERT_STATUS_ALL = 'all'; export const TIMESTAMP_FIELD = '@timestamp'; export const DATA_VIEW_PREFIX = 'infra_metrics'; +export const DEFAULT_HOST_LIMIT: HostLimitOptions = 100; +export const DEFAULT_PAGE_SIZE = 10; +export const LOCAL_STORAGE_HOST_LIMIT_KEY = 'hostsView:hostLimitSelection'; +export const LOCAL_STORAGE_PAGE_SIZE_KEY = 'hostsView:pageSizeSelection'; + export const ALL_ALERTS: AlertStatusFilter = { status: ALERT_STATUS_ALL, label: i18n.translate('xpack.infra.hostsViewPage.tabs.alerts.alertStatusFilter.showAll', { @@ -52,3 +57,5 @@ export const ALERT_STATUS_QUERY = { [ACTIVE_ALERTS.status]: ACTIVE_ALERTS.query, [RECOVERED_ALERTS.status]: RECOVERED_ALERTS.query, }; + +export const HOST_LIMIT_OPTIONS = [10, 20, 50, 100, 500] as const; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts new file mode 100644 index 0000000000000..8c9a84d4402f8 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_after_loaded_state.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef } from 'react'; + +export const useAfterLoadedState = (loading: boolean, state: T) => { + const ref = useRef(undefined); + const [internalState, setInternalState] = useState(state); + + if (!ref.current || loading !== ref.current) { + ref.current = loading; + } + + useEffect(() => { + if (!loading) { + setInternalState(state); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ref.current]); + + return { afterLoadedState: internalState }; +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts index 9877d61643721..200bff521d86a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_alerts_query.ts @@ -9,7 +9,7 @@ import createContainer from 'constate'; import { getTime } from '@kbn/data-plugin/common'; import { ALERT_TIME_RANGE } from '@kbn/rule-data-utils'; import { BoolQuery, buildEsQuery, Filter } from '@kbn/es-query'; -import { SnapshotNode } from '../../../../../common/http_api'; +import { InfraAssetMetricsItem } from '../../../../../common/http_api'; import { useUnifiedSearchContext } from './use_unified_search'; import { HostsState } from './use_unified_search_url_state'; import { useHostsViewContext } from './use_hosts_view'; @@ -63,13 +63,13 @@ const createAlertsEsQuery = ({ status, }: { dateRange: HostsState['dateRange']; - hostNodes: SnapshotNode[]; + hostNodes: InfraAssetMetricsItem[]; status?: AlertStatus; }): AlertsEsQuery => { const alertStatusFilter = createAlertStatusFilter(status); const dateFilter = createDateFilter(dateRange); - const hostsFilter = createHostsFilter(hostNodes); + const hostsFilter = createHostsFilter(hostNodes.map((p) => p.name)); const filters = [alertStatusFilter, dateFilter, hostsFilter].filter(Boolean) as Filter[]; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts index 94e3a963075be..83fedf4292937 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_data_view.ts @@ -72,6 +72,7 @@ export const useDataView = ({ metricAlias }: { metricAlias: string }) => { }, [hasError, notifications, metricAlias]); return { + metricAlias, dataView, loading, hasError, diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts new file mode 100644 index 0000000000000..5575c46e621f1 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_count.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; +import { ES_SEARCH_STRATEGY, IKibanaSearchResponse } from '@kbn/data-plugin/common'; +import { useCallback, useEffect } from 'react'; +import { catchError, map, Observable, of, startWith } from 'rxjs'; +import createContainer from 'constate'; +import type { QueryDslQueryContainer, SearchResponse } from '@elastic/elasticsearch/lib/api/types'; +import { decodeOrThrow } from '../../../../../common/runtime_types'; +import { useDataSearch, useLatestPartialDataSearchResponse } from '../../../../utils/data_search'; +import { useMetricsDataViewContext } from './use_data_view'; +import { useUnifiedSearchContext } from './use_unified_search'; + +export const useHostCount = () => { + const { dataView, metricAlias } = useMetricsDataViewContext(); + const { buildQuery, getParsedDateRange } = useUnifiedSearchContext(); + + const { search: fetchHostCount, requests$ } = useDataSearch({ + getRequest: useCallback(() => { + const query = buildQuery(); + const dateRange = getParsedDateRange(); + + const filters: QueryDslQueryContainer = { + bool: { + ...query.bool, + filter: [ + ...query.bool.filter, + { + exists: { + field: 'host.name', + }, + }, + { + range: { + [dataView?.timeFieldName ?? '@timestamp']: { + gte: dateRange.from, + lte: dateRange.to, + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }; + + return { + request: { + params: { + allow_no_indices: true, + ignore_unavailable: true, + index: metricAlias, + size: 0, + track_total_hits: false, + body: { + query: filters, + aggs: { + count: { + cardinality: { + field: 'host.name', + }, + }, + }, + }, + }, + }, + options: { strategy: ES_SEARCH_STRATEGY }, + }; + }, [buildQuery, dataView, getParsedDateRange, metricAlias]), + parseResponses: normalizeDataSearchResponse, + }); + + const { isRequestRunning, isResponsePartial, latestResponseData, latestResponseErrors } = + useLatestPartialDataSearchResponse(requests$); + + useEffect(() => { + fetchHostCount(); + }, [fetchHostCount]); + + return { + errors: latestResponseErrors, + isRequestRunning, + isResponsePartial, + data: latestResponseData ?? null, + }; +}; + +export const HostCount = createContainer(useHostCount); +export const [HostCountProvider, useHostCountContext] = HostCount; + +const INITIAL_STATE = { + data: null, + errors: [], + isPartial: true, + isRunning: true, + loaded: 0, + total: undefined, +}; +const normalizeDataSearchResponse = ( + response$: Observable>>> +) => + response$.pipe( + map((response) => ({ + data: decodeOrThrow(HostCountResponseRT)(response.rawResponse.aggregations), + errors: [], + isPartial: response.isPartial ?? false, + isRunning: response.isRunning ?? false, + loaded: response.loaded, + total: response.total, + })), + startWith(INITIAL_STATE), + catchError((error) => + of({ + ...INITIAL_STATE, + errors: [error.message ?? error], + isRunning: false, + }) + ) + ); + +const HostCountResponseRT = rt.type({ + count: rt.type({ + value: rt.number, + }), +}); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts index 635fe556e85e5..663ecf3a92643 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_host_flyout_open_url_state.ts @@ -25,19 +25,29 @@ export const GET_DEFAULT_TABLE_PROPERTIES = { const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'hostFlyoutOpen'; type Action = rt.TypeOf; -type SetNewHostFlyoutOpen = (newProps: Action) => void; - -export const useHostFlyoutOpen = (): [HostFlyoutOpen, SetNewHostFlyoutOpen] => { - const [urlState, setUrlState] = useUrlState({ - defaultState: GET_DEFAULT_TABLE_PROPERTIES, +type SetNewHostFlyoutOpen = (newProp: Action) => void; +type SetNewHostFlyoutClose = () => void; + +export const useHostFlyoutOpen = (): [ + HostFlyoutOpen, + SetNewHostFlyoutOpen, + SetNewHostFlyoutClose +] => { + const [urlState, setUrlState] = useUrlState({ + defaultState: '', decodeUrlState, encodeUrlState, urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, }); - const setHostFlyoutOpen = (newProps: Action) => setUrlState({ ...urlState, ...newProps }); + const setHostFlyoutOpen = (newProps: Action) => + typeof urlState !== 'string' + ? setUrlState({ ...urlState, ...newProps }) + : setUrlState({ ...GET_DEFAULT_TABLE_PROPERTIES, ...newProps }); + + const setFlyoutClosed = () => setUrlState(''); - return [urlState, setHostFlyoutOpen]; + return [urlState as HostFlyoutOpen, setHostFlyoutOpen, setFlyoutClosed]; }; const FlyoutTabIdRT = rt.union([rt.literal('metadata'), rt.literal('processes')]); @@ -74,9 +84,12 @@ const HostFlyoutOpenRT = rt.type({ metadataSearch: SearchFilterRT, }); +const HostFlyoutUrlRT = rt.union([HostFlyoutOpenRT, rt.string]); + +type HostFlyoutUrl = rt.TypeOf; type HostFlyoutOpen = rt.TypeOf; -const encodeUrlState = HostFlyoutOpenRT.encode; +const encodeUrlState = HostFlyoutUrlRT.encode; const decodeUrlState = (value: unknown) => { - return pipe(HostFlyoutOpenRT.decode(value), fold(constant(undefined), identity)); + return pipe(HostFlyoutUrlRT.decode(value), fold(constant('undefined'), identity)); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts index 4ae8823adaf2e..5619a788b19a7 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.test.ts @@ -7,69 +7,96 @@ import { useHostsTable } from './use_hosts_table'; import { renderHook } from '@testing-library/react-hooks'; -import { SnapshotNode } from '../../../../../common/http_api'; +import { InfraAssetMetricsItem } from '../../../../../common/http_api'; +import * as useUnifiedSearchHooks from './use_unified_search'; +import * as useHostsViewHooks from './use_hosts_view'; -describe('useHostTable hook', () => { - it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { - const nodes: SnapshotNode[] = [ +jest.mock('./use_unified_search'); +jest.mock('./use_hosts_view'); + +const mockUseUnifiedSearchContext = + useUnifiedSearchHooks.useUnifiedSearchContext as jest.MockedFunction< + typeof useUnifiedSearchHooks.useUnifiedSearchContext + >; +const mockUseHostsViewContext = useHostsViewHooks.useHostsViewContext as jest.MockedFunction< + typeof useHostsViewHooks.useHostsViewContext +>; + +const mockHostNode: InfraAssetMetricsItem[] = [ + { + metrics: [ { - metrics: [ - { - name: 'rx', - avg: 252456.92916666667, - }, - { - name: 'tx', - avg: 252758.425, - }, - { - name: 'memory', - avg: 0.94525, - }, - { - name: 'cpu', - value: 0.6353277777777777, - }, - { - name: 'memoryTotal', - avg: 34359.738368, - }, - ], - path: [{ value: 'host-0', label: 'host-0', os: null, cloudProvider: 'aws' }], - name: 'host-0', + name: 'rx', + value: 252456.92916666667, }, { - metrics: [ - { - name: 'rx', - avg: 95.86339715321859, - }, - { - name: 'tx', - avg: 110.38566859563191, - }, - { - name: 'memory', - avg: 0.5400000214576721, - }, - { - name: 'cpu', - value: 0.8647805555555556, - }, - { - name: 'memoryTotal', - avg: 9.194304, - }, - ], - path: [ - { value: 'host-1', label: 'host-1' }, - { value: 'host-1', label: 'host-1', ip: '243.86.94.22', os: 'macOS' }, - ], - name: 'host-1', + name: 'tx', + value: 252758.425, }, - ]; + { + name: 'memory', + value: 0.94525, + }, + { + name: 'cpu', + value: 0.6353277777777777, + }, + { + name: 'memoryTotal', + value: 34359.738368, + }, + ], + metadata: [ + { name: 'host.os.name', value: null }, + { name: 'cloud.provider', value: 'aws' }, + ], + name: 'host-0', + }, + { + metrics: [ + { + name: 'rx', + value: 95.86339715321859, + }, + { + name: 'tx', + value: 110.38566859563191, + }, + { + name: 'memory', + value: 0.5400000214576721, + }, + { + name: 'cpu', + value: 0.8647805555555556, + }, + { + name: 'memoryTotal', + value: 9.194304, + }, + ], + metadata: [ + { name: 'host.os.name', value: 'macOS' }, + { name: 'host.ip', value: '243.86.94.22' }, + ], + name: 'host-1', + }, +]; + +describe('useHostTable hook', () => { + beforeAll(() => { + mockUseUnifiedSearchContext.mockReturnValue({ + searchCriteria: { + dateRange: { from: 'now-15m', to: 'now' }, + }, + } as ReturnType); - const items = [ + mockUseHostsViewContext.mockReturnValue({ + hostNodes: mockHostNode, + } as ReturnType); + }); + it('it should map the nodes returned from the snapshot api to a format matching eui table items', () => { + const expected = [ { name: 'host-0', os: '-', @@ -79,27 +106,11 @@ describe('useHostTable hook', () => { cloudProvider: 'aws', name: 'host-0', }, - rx: { - name: 'rx', - avg: 252456.92916666667, - }, - tx: { - name: 'tx', - avg: 252758.425, - }, - memory: { - name: 'memory', - avg: 0.94525, - }, - cpu: { - name: 'cpu', - value: 0.6353277777777777, - }, - memoryTotal: { - name: 'memoryTotal', - - avg: 34359.738368, - }, + rx: 252456.92916666667, + tx: 252758.425, + memory: 0.94525, + cpu: 0.6353277777777777, + memoryTotal: 34359.738368, }, { name: 'host-1', @@ -110,32 +121,16 @@ describe('useHostTable hook', () => { cloudProvider: null, name: 'host-1', }, - rx: { - name: 'rx', - avg: 95.86339715321859, - }, - tx: { - name: 'tx', - avg: 110.38566859563191, - }, - memory: { - name: 'memory', - avg: 0.5400000214576721, - }, - cpu: { - name: 'cpu', - value: 0.8647805555555556, - }, - memoryTotal: { - name: 'memoryTotal', - avg: 9.194304, - }, + rx: 95.86339715321859, + tx: 110.38566859563191, + memory: 0.5400000214576721, + cpu: 0.8647805555555556, + memoryTotal: 9.194304, }, ]; - const time = { from: 'now-15m', to: 'now', interval: '>=1m' }; - const { result } = renderHook(() => useHostsTable(nodes, { time })); + const { result } = renderHook(() => useHostsTable()); - expect(result.current.items).toStrictEqual(items); + expect(result.current.items).toStrictEqual(expected); }); }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx index 0b7021b97f84c..7350f402c57ec 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table.tsx @@ -8,64 +8,107 @@ import React, { useCallback, useMemo } from 'react'; import { EuiBasicTableColumn, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { TimeRange } from '@kbn/es-query'; - +import createContainer from 'constate'; +import { isEqual } from 'lodash'; +import { CriteriaWithPagination } from '@elastic/eui'; +import { isNumber } from 'lodash/fp'; import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { createInventoryMetricFormatter } from '../../inventory_view/lib/create_inventory_metric_formatter'; import { HostsTableEntryTitle } from '../components/hosts_table_entry_title'; -import type { - SnapshotNode, - SnapshotNodeMetric, - SnapshotMetricInput, +import { + InfraAssetMetadataType, + InfraAssetMetricsItem, + InfraAssetMetricType, } from '../../../../../common/http_api'; import { useHostFlyoutOpen } from './use_host_flyout_open_url_state'; +import { Sorting, useHostsTableProperties } from './use_hosts_table_url_state'; +import { useHostsViewContext } from './use_hosts_view'; +import { useUnifiedSearchContext } from './use_unified_search'; /** * Columns and items types */ export type CloudProvider = 'gcp' | 'aws' | 'azure' | 'unknownProvider'; +type HostMetrics = Record; -type HostMetric = 'cpu' | 'diskLatency' | 'rx' | 'tx' | 'memory' | 'memoryTotal'; - -type HostMetrics = Record; - -export interface HostNodeRow extends HostMetrics { +interface HostMetadata { os?: string | null; ip?: string | null; servicesOnHost?: number | null; title: { name: string; cloudProvider?: CloudProvider | null }; - name: string; id: string; } - -interface HostTableParams { - time: TimeRange; -} +export type HostNodeRow = HostMetadata & + HostMetrics & { + name: string; + }; /** * Helper functions */ -const formatMetric = (type: SnapshotMetricInput['type'], value: number | undefined | null) => { +const formatMetric = (type: InfraAssetMetricType, value: number | undefined | null) => { return value || value === 0 ? createInventoryMetricFormatter({ type })(value) : 'N/A'; }; -const buildItemsList = (nodes: SnapshotNode[]) => { - return nodes.map(({ metrics, path, name }) => ({ - id: `${name}-${path.at(-1)?.os ?? '-'}`, - name, - os: path.at(-1)?.os ?? '-', - ip: path.at(-1)?.ip ?? '', - title: { +const buildItemsList = (nodes: InfraAssetMetricsItem[]): HostNodeRow[] => { + return nodes.map(({ metrics, metadata, name }) => { + const metadataKeyValue = metadata.reduce( + (acc, curr) => ({ + ...acc, + [curr.name]: curr.value, + }), + {} as Record + ); + + return { name, - cloudProvider: path.at(-1)?.cloudProvider ?? null, - }, - ...metrics.reduce((data, metric) => { - data[metric.name as HostMetric] = metric; - return data; - }, {} as HostMetrics), - })) as HostNodeRow[]; + id: `${name}-${metadataKeyValue['host.os.name'] ?? '-'}`, + title: { + name, + cloudProvider: (metadataKeyValue['cloud.provider'] as CloudProvider) ?? null, + }, + os: metadataKeyValue['host.os.name'] ?? '-', + ip: metadataKeyValue['host.ip'] ?? '', + ...metrics.reduce( + (acc, curr) => ({ + ...acc, + [curr.name]: curr.value ?? 0, + }), + {} as HostMetrics + ), + }; + }); +}; + +const isTitleColumn = (cell: any): cell is HostNodeRow['title'] => { + return typeof cell === 'object' && cell && 'name' in cell; +}; + +const sortValues = (aValue: any, bValue: any, { direction }: Sorting) => { + if (typeof aValue === 'string' && typeof bValue === 'string') { + return direction === 'desc' ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue); + } + + if (isNumber(aValue) && isNumber(bValue)) { + return direction === 'desc' ? bValue - aValue : aValue - bValue; + } + + return 1; }; +const sortTableData = + ({ direction, field }: Sorting) => + (a: HostNodeRow, b: HostNodeRow) => { + const aValue = a[field as keyof HostNodeRow]; + const bValue = b[field as keyof HostNodeRow]; + + if (isTitleColumn(aValue) && isTitleColumn(bValue)) { + return sortValues(aValue.name, bValue.name, { direction, field }); + } + + return sortValues(aValue, bValue, { direction, field }); + }; + /** * Columns translations */ @@ -120,14 +163,17 @@ const toggleDialogActionLabel = i18n.translate( /** * Build a table columns and items starting from the snapshot nodes. */ -export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) => { +export const useHostsTable = () => { + const { hostNodes } = useHostsViewContext(); + const { searchCriteria } = useUnifiedSearchContext(); + const [{ pagination, sorting }, setProperties] = useHostsTableProperties(); const { services: { telemetry }, } = useKibanaContextForPlugin(); - const [hostFlyoutOpen, setHostFlyoutOpen] = useHostFlyoutOpen(); + const [hostFlyoutOpen, setHostFlyoutOpen, setFlyoutClosed] = useHostFlyoutOpen(); - const closeFlyout = () => setHostFlyoutOpen({ clickedItemId: '' }); + const closeFlyout = () => setFlyoutClosed(); const reportHostEntryClick = useCallback( ({ name, cloudProvider }: HostNodeRow['title']) => { @@ -139,12 +185,38 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) [telemetry] ); - const items = useMemo(() => buildItemsList(nodes), [nodes]); + const onTableChange = useCallback( + ({ page, sort }: CriteriaWithPagination) => { + const { index: pageIndex, size: pageSize } = page; + const { field, direction } = sort ?? {}; + + const currentSorting = { field: field as keyof HostNodeRow, direction }; + const currentPagination = { pageIndex, pageSize }; + + if (!isEqual(sorting, currentSorting)) { + setProperties({ sorting: currentSorting }); + } else if (!isEqual(pagination, currentPagination)) { + setProperties({ pagination: currentPagination }); + } + }, + [setProperties, pagination, sorting] + ); + + const items = useMemo(() => buildItemsList(hostNodes), [hostNodes]); const clickedItem = useMemo( () => items.find(({ id }) => id === hostFlyoutOpen.clickedItemId), [hostFlyoutOpen.clickedItemId, items] ); + const currentPage = useMemo(() => { + const { pageSize = 0, pageIndex = 0 } = pagination; + + const endIndex = (pageIndex + 1) * pageSize; + const startIndex = pageIndex * pageSize; + + return items.sort(sortTableData(sorting)).slice(startIndex, endIndex); + }, [items, pagination, sorting]); + const columns: Array> = useMemo( () => [ { @@ -166,7 +238,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) clickedItemId: id, }); if (id === hostFlyoutOpen.clickedItemId) { - setHostFlyoutOpen({ clickedItemId: '' }); + setFlyoutClosed(); } else { setHostFlyoutOpen({ clickedItemId: id }); } @@ -183,7 +255,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) render: (title: HostNodeRow['title']) => ( reportHostEntryClick(title)} /> ), @@ -197,7 +269,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageCpuUsageLabel, - field: 'cpu.avg', + field: 'cpu', sortable: true, 'data-test-subj': 'hostsView-tableRow-cpuUsage', render: (avg: number) => formatMetric('cpu', avg), @@ -205,7 +277,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: diskLatencyLabel, - field: 'diskLatency.avg', + field: 'diskLatency', sortable: true, 'data-test-subj': 'hostsView-tableRow-diskLatency', render: (avg: number) => formatMetric('diskLatency', avg), @@ -213,7 +285,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageRXLabel, - field: 'rx.avg', + field: 'rx', sortable: true, 'data-test-subj': 'hostsView-tableRow-rx', render: (avg: number) => formatMetric('rx', avg), @@ -221,7 +293,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTXLabel, - field: 'tx.avg', + field: 'tx', sortable: true, 'data-test-subj': 'hostsView-tableRow-tx', render: (avg: number) => formatMetric('tx', avg), @@ -229,7 +301,7 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageTotalMemoryLabel, - field: 'memoryTotal.avg', + field: 'memoryTotal', sortable: true, 'data-test-subj': 'hostsView-tableRow-memoryTotal', render: (avg: number) => formatMetric('memoryTotal', avg), @@ -237,21 +309,34 @@ export const useHostsTable = (nodes: SnapshotNode[], { time }: HostTableParams) }, { name: averageMemoryUsageLabel, - field: 'memory.avg', + field: 'memory', sortable: true, 'data-test-subj': 'hostsView-tableRow-memory', render: (avg: number) => formatMetric('memory', avg), align: 'right', }, ], - [hostFlyoutOpen.clickedItemId, reportHostEntryClick, setHostFlyoutOpen, time] + [ + hostFlyoutOpen.clickedItemId, + reportHostEntryClick, + searchCriteria.dateRange, + setFlyoutClosed, + setHostFlyoutOpen, + ] ); return { columns, - items, clickedItem, - isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + currentPage, closeFlyout, + items, + isFlyoutOpen: !!hostFlyoutOpen.clickedItemId, + onTableChange, + pagination, + sorting, }; }; + +export const HostsTable = createContainer(useHostsTable); +export const [HostsTableProvider, useHostsTableContext] = HostsTable; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts new file mode 100644 index 0000000000000..b4889d62f5878 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_table_url_state.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { fold } from 'fp-ts/lib/Either'; +import { constant, identity } from 'fp-ts/lib/function'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; +import deepEqual from 'fast-deep-equal'; +import { useReducer } from 'react'; +import { useUrlState } from '../../../../utils/use_url_state'; +import { DEFAULT_PAGE_SIZE, LOCAL_STORAGE_PAGE_SIZE_KEY } from '../constants'; + +export const GET_DEFAULT_TABLE_PROPERTIES: TableProperties = { + sorting: { + direction: 'asc', + field: 'name', + }, + pagination: { + pageIndex: 0, + pageSize: DEFAULT_PAGE_SIZE, + }, +}; + +const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; + +const reducer = (prevState: TableProperties, params: Payload) => { + const payload = Object.fromEntries(Object.entries(params).filter(([_, v]) => !!v)); + + return { + ...prevState, + ...payload, + }; +}; + +export const useHostsTableProperties = (): [TableProperties, TablePropertiesUpdater] => { + const [localStoragePageSize, setLocalStoragePageSize] = useLocalStorage( + LOCAL_STORAGE_PAGE_SIZE_KEY, + DEFAULT_PAGE_SIZE + ); + + const [urlState, setUrlState] = useUrlState({ + defaultState: { + ...GET_DEFAULT_TABLE_PROPERTIES, + pagination: { + ...GET_DEFAULT_TABLE_PROPERTIES.pagination, + pageSize: localStoragePageSize, + }, + }, + + decodeUrlState, + encodeUrlState, + urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, + }); + + const [properties, setProperties] = useReducer(reducer, urlState); + if (!deepEqual(properties, urlState)) { + setUrlState(properties); + if (localStoragePageSize !== properties.pagination.pageSize) { + setLocalStoragePageSize(properties.pagination.pageSize); + } + } + + return [properties, setProperties]; +}; + +const PaginationRT = rt.partial({ pageIndex: rt.number, pageSize: rt.number }); +const SortingRT = rt.intersection([ + rt.type({ + field: rt.string, + }), + rt.partial({ direction: rt.union([rt.literal('asc'), rt.literal('desc')]) }), +]); + +const TableStateRT = rt.type({ + pagination: PaginationRT, + sorting: SortingRT, +}); + +export type TableState = rt.TypeOf; +export type Payload = Partial; +export type TablePropertiesUpdater = (params: Payload) => void; + +export type Sorting = rt.TypeOf; +type TableProperties = rt.TypeOf; + +const encodeUrlState = TableStateRT.encode; +const decodeUrlState = (value: unknown) => { + return pipe(TableStateRT.decode(value), fold(constant(undefined), identity)); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts index d0df961dc7ef9..f84acf5931ea1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_hosts_view.ts @@ -12,16 +12,21 @@ * 2.0. */ -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import createContainer from 'constate'; import { BoolQuery } from '@kbn/es-query'; -import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import { useKibanaContextForPlugin } from '../../../../hooks/use_kibana'; import { useSourceContext } from '../../../../containers/metrics_source'; -import { useSnapshot, type UseSnapshotRequest } from '../../inventory_view/hooks/use_snaphot'; import { useUnifiedSearchContext } from './use_unified_search'; -import { StringDateRangeTimestamp } from './use_unified_search_url_state'; +import { + GetInfraMetricsRequestBodyPayload, + GetInfraMetricsResponsePayload, + InfraAssetMetricType, +} from '../../../../../common/http_api'; +import { StringDateRange } from './use_unified_search_url_state'; -const HOST_TABLE_METRICS: Array<{ type: SnapshotMetricType }> = [ +const HOST_TABLE_METRICS: Array<{ type: InfraAssetMetricType }> = [ { type: 'rx' }, { type: 'tx' }, { type: 'memory' }, @@ -30,40 +35,52 @@ const HOST_TABLE_METRICS: Array<{ type: SnapshotMetricType }> = [ { type: 'memoryTotal' }, ]; +const BASE_INFRA_METRICS_PATH = '/api/metrics/infra'; + export const useHostsView = () => { const { sourceId } = useSourceContext(); - const { buildQuery, getDateRangeAsTimestamp } = useUnifiedSearchContext(); + const { + services: { http }, + } = useKibanaContextForPlugin(); + const { buildQuery, getParsedDateRange, searchCriteria } = useUnifiedSearchContext(); + const abortCtrlRef = useRef(new AbortController()); const baseRequest = useMemo( () => - createSnapshotRequest({ - dateRange: getDateRangeAsTimestamp(), + createInfraMetricsRequest({ + dateRange: getParsedDateRange(), esQuery: buildQuery(), sourceId, + limit: searchCriteria.limit, }), - [buildQuery, getDateRangeAsTimestamp, sourceId] + [buildQuery, getParsedDateRange, sourceId, searchCriteria.limit] ); - // Snapshot endpoint internally uses the indices stored in source.configuration.metricAlias. - // For the Unified Search, we create a data view, which for now will be built off of source.configuration.metricAlias too - // if we introduce data view selection, we'll have to change this hook and the endpoint to accept a new parameter for the indices - const { - loading, - error, - nodes: hostNodes, - } = useSnapshot( - { - ...baseRequest, - metrics: HOST_TABLE_METRICS, + const [state, refetch] = useAsyncFn( + () => { + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + + return http.post(`${BASE_INFRA_METRICS_PATH}`, { + signal: abortCtrlRef.current.signal, + body: JSON.stringify(baseRequest), + }); }, - { abortable: true } + [baseRequest, http], + { loading: true } ); + useEffect(() => { + refetch(); + }, [refetch]); + + const { value, error, loading } = state; + return { - baseRequest, + requestTs: baseRequest.requestTs, loading, error, - hostNodes, + hostNodes: value?.nodes ?? [], }; }; @@ -73,30 +90,26 @@ export const [HostsViewProvider, useHostsViewContext] = HostsView; /** * Helpers */ -const createSnapshotRequest = ({ + +const createInfraMetricsRequest = ({ esQuery, sourceId, dateRange, + limit, }: { esQuery: { bool: BoolQuery }; sourceId: string; - dateRange: StringDateRangeTimestamp; -}): UseSnapshotRequest => ({ - filterQuery: JSON.stringify(esQuery), - metrics: [], - groupBy: [], - nodeType: 'host', - sourceId, - currentTime: dateRange.to, - includeTimeseries: false, - sendRequestImmediately: true, - timerange: { - interval: '1m', + dateRange: StringDateRange; + limit: number; +}): GetInfraMetricsRequestBodyPayload & { requestTs: number } => ({ + type: 'host', + query: esQuery, + range: { from: dateRange.from, to: dateRange.to, - ignoreLookback: true, }, - // The user might want to click on the submit button without changing the filters - // This makes sure all child components will re-render. + metrics: HOST_TABLE_METRICS, + limit, + sourceId, requestTs: Date.now(), }); diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts deleted file mode 100644 index 980fdf19a684c..0000000000000 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_table_properties_url_state.ts +++ /dev/null @@ -1,62 +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'; -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { constant, identity } from 'fp-ts/lib/function'; -import { useUrlState } from '../../../../utils/use_url_state'; - -export const GET_DEFAULT_TABLE_PROPERTIES = { - sorting: true, - pagination: true, -}; -const HOST_TABLE_PROPERTIES_URL_STATE_KEY = 'tableProperties'; - -type Action = rt.TypeOf; -type PropertiesUpdater = (newProps: Action) => void; - -export const useTableProperties = (): [TableProperties, PropertiesUpdater] => { - const [urlState, setUrlState] = useUrlState({ - defaultState: GET_DEFAULT_TABLE_PROPERTIES, - decodeUrlState, - encodeUrlState, - urlStateKey: HOST_TABLE_PROPERTIES_URL_STATE_KEY, - }); - - const setProperties = (newProps: Action) => setUrlState({ ...urlState, ...newProps }); - - return [urlState, setProperties]; -}; - -const PaginationRT = rt.union([ - rt.boolean, - rt.partial({ pageIndex: rt.number, pageSize: rt.number }), -]); -const SortingRT = rt.union([rt.boolean, rt.type({ field: rt.string, direction: rt.any })]); - -const SetSortingRT = rt.partial({ - sorting: SortingRT, -}); - -const SetPaginationRT = rt.partial({ - pagination: PaginationRT, -}); - -const ActionRT = rt.intersection([SetSortingRT, SetPaginationRT]); - -const TablePropertiesRT = rt.type({ - pagination: PaginationRT, - sorting: SortingRT, -}); - -type TableProperties = rt.TypeOf; - -const encodeUrlState = TablePropertiesRT.encode; -const decodeUrlState = (value: unknown) => { - return pipe(TablePropertiesRT.decode(value), fold(constant(undefined), identity)); -}; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts index 950bd9cf3c94e..e242f58054c6c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search.ts @@ -23,14 +23,14 @@ import { } from './use_unified_search_url_state'; const buildQuerySubmittedPayload = ( - hostState: HostsState & { dateRangeTimestamp: StringDateRangeTimestamp } + hostState: HostsState & { parsedDateRange: StringDateRangeTimestamp } ) => { - const { panelFilters, filters, dateRangeTimestamp, query: queryObj } = hostState; + const { panelFilters, filters, parsedDateRange, query: queryObj } = hostState; return { control_filters: panelFilters.map((filter) => JSON.stringify(filter)), filters: filters.map((filter) => JSON.stringify(filter)), - interval: telemetryTimeRangeFormatter(dateRangeTimestamp.to - dateRangeTimestamp.from), + interval: telemetryTimeRangeFormatter(parsedDateRange.to - parsedDateRange.from), query: queryObj.query, }; }; @@ -41,8 +41,8 @@ const getDefaultTimestamps = () => { const now = Date.now(); return { - from: now - DEFAULT_FROM_IN_MILLISECONDS, - to: now, + from: new Date(now - DEFAULT_FROM_IN_MILLISECONDS).toISOString(), + to: new Date(now).toISOString(), }; }; @@ -63,16 +63,25 @@ export const useUnifiedSearch = () => { const onSubmit = (params?: HostsSearchPayload) => setSearch(params ?? {}); - const getDateRangeAsTimestamp = useCallback(() => { + const getParsedDateRange = useCallback(() => { const defaults = getDefaultTimestamps(); - const from = DateMath.parse(searchCriteria.dateRange.from)?.valueOf() ?? defaults.from; + const from = DateMath.parse(searchCriteria.dateRange.from)?.toISOString() ?? defaults.from; const to = - DateMath.parse(searchCriteria.dateRange.to, { roundUp: true })?.valueOf() ?? defaults.to; + DateMath.parse(searchCriteria.dateRange.to, { roundUp: true })?.toISOString() ?? defaults.to; return { from, to }; }, [searchCriteria.dateRange]); + const getDateRangeAsTimestamp = useCallback(() => { + const parsedDate = getParsedDateRange(); + + const from = new Date(parsedDate.from).getTime(); + const to = new Date(parsedDate.to).getTime(); + + return { from, to }; + }, [getParsedDateRange]); + const buildQuery = useCallback(() => { return buildEsQuery(dataView, searchCriteria.query, [ ...searchCriteria.filters, @@ -116,15 +125,16 @@ export const useUnifiedSearch = () => { // Track telemetry event on query/filter/date changes useEffect(() => { - const dateRangeTimestamp = getDateRangeAsTimestamp(); + const parsedDateRange = getDateRangeAsTimestamp(); telemetry.reportHostsViewQuerySubmitted( - buildQuerySubmittedPayload({ ...searchCriteria, dateRangeTimestamp }) + buildQuerySubmittedPayload({ ...searchCriteria, parsedDateRange }) ); }, [getDateRangeAsTimestamp, searchCriteria, telemetry]); return { buildQuery, onSubmit, + getParsedDateRange, getDateRangeAsTimestamp, searchCriteria, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts index 861f3c26472e8..bae9f2ed3f713 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/hooks/use_unified_search_url_state.ts @@ -13,11 +13,13 @@ import { fold } from 'fp-ts/lib/Either'; import { constant, identity } from 'fp-ts/lib/function'; import { enumeration } from '@kbn/securitysolution-io-ts-types'; import { FilterStateStore } from '@kbn/es-query'; +import useLocalStorage from 'react-use/lib/useLocalStorage'; import { useUrlState } from '../../../../utils/use_url_state'; import { useKibanaTimefilterTime, useSyncKibanaTimeFilterTime, } from '../../../../hooks/use_kibana_timefilter_time'; +import { DEFAULT_HOST_LIMIT, LOCAL_STORAGE_HOST_LIMIT_KEY } from '../constants'; const DEFAULT_QUERY = { language: 'kuery', @@ -32,6 +34,7 @@ const INITIAL_HOSTS_STATE: HostsState = { filters: [], panelFilters: [], dateRange: INITIAL_DATE_RANGE, + limit: DEFAULT_HOST_LIMIT, }; const reducer = (prevState: HostsState, params: HostsSearchPayload) => { @@ -45,9 +48,17 @@ const reducer = (prevState: HostsState, params: HostsSearchPayload) => { export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => { const [getTime] = useKibanaTimefilterTime(INITIAL_DATE_RANGE); + const [localStorageHostLimit, setLocalStorageHostLimit] = useLocalStorage( + LOCAL_STORAGE_HOST_LIMIT_KEY, + INITIAL_HOSTS_STATE.limit + ); const [urlState, setUrlState] = useUrlState({ - defaultState: { ...INITIAL_HOSTS_STATE, dateRange: getTime() }, + defaultState: { + ...INITIAL_HOSTS_STATE, + dateRange: getTime(), + limit: localStorageHostLimit ?? INITIAL_HOSTS_STATE.limit, + }, decodeUrlState, encodeUrlState, urlStateKey: '_a', @@ -57,6 +68,9 @@ export const useHostsUrlState = (): [HostsState, HostsStateUpdater] => { const [search, setSearch] = useReducer(reducer, urlState); if (!deepEqual(search, urlState)) { setUrlState(search); + if (localStorageHostLimit !== search.limit) { + setLocalStorageHostLimit(search.limit); + } } useSyncKibanaTimeFilterTime(INITIAL_DATE_RANGE, urlState.dateRange, (dateRange) => @@ -110,6 +124,7 @@ const HostsStateRT = rt.type({ panelFilters: HostsFiltersRT, query: HostsQueryStateRT, dateRange: StringDateRangeRT, + limit: rt.number, }); export type HostsState = rt.TypeOf; @@ -118,6 +133,7 @@ export type HostsSearchPayload = Partial; export type HostsStateUpdater = (params: HostsSearchPayload) => void; +export type StringDateRange = rt.TypeOf; export interface StringDateRangeTimestamp { from: number; to: number; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts index 6b948fb0da6c9..080b47f54d4da 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/types.ts @@ -7,7 +7,7 @@ import { Filter } from '@kbn/es-query'; import { ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED } from '@kbn/rule-data-utils'; -import { ALERT_STATUS_ALL } from './constants'; +import { ALERT_STATUS_ALL, HOST_LIMIT_OPTIONS } from './constants'; export type AlertStatus = | typeof ALERT_STATUS_ACTIVE @@ -19,3 +19,5 @@ export interface AlertStatusFilter { query?: Filter['query']; label: string; } + +export type HostLimitOptions = typeof HOST_LIMIT_OPTIONS[number]; diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts index a04fdfa46b279..5da9d36b0f587 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/utils.ts @@ -5,16 +5,23 @@ * 2.0. */ -import { Filter } from '@kbn/es-query'; -import { SnapshotNode } from '../../../../common/http_api'; +import { DataViewBase, Filter } from '@kbn/es-query'; -export const createHostsFilter = (hostNodes: SnapshotNode[]): Filter => { +export const createHostsFilter = (hostNames: string[], dataView?: DataViewBase): Filter => { return { query: { terms: { - 'host.name': hostNodes.map((p) => p.name), + 'host.name': hostNames, }, }, - meta: {}, + meta: dataView + ? { + value: hostNames.join(), + type: 'phrases', + params: hostNames, + index: dataView.id, + key: 'host.name', + } + : {}, }; }; diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index bdd4f7d841217..3b6ea0333f236 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -8,6 +8,7 @@ import { InfraBackendLibs } from './lib/infra_types'; import { initGetHostsAnomaliesRoute, initGetK8sAnomaliesRoute } from './routes/infra_ml'; import { initInventoryMetaRoute } from './routes/inventory_metadata'; +import { initInventoryViewRoutes } from './routes/inventory_views'; import { initIpToHostName } from './routes/ip_to_hostname'; import { initGetLogAlertsChartPreviewDataRoute } from './routes/log_alerts'; import { @@ -61,6 +62,7 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initMetricsAPIRoute(libs); initMetadataRoute(libs); initInventoryMetaRoute(libs); + initInventoryViewRoutes(libs); initGetLogAlertsChartPreviewDataRoute(libs); initProcessListRoute(libs); initOverviewRoute(libs); diff --git a/x-pack/plugins/infra/server/mocks.ts b/x-pack/plugins/infra/server/mocks.ts index 5b587a1fe80d5..5a97f4a7d9a52 100644 --- a/x-pack/plugins/infra/server/mocks.ts +++ b/x-pack/plugins/infra/server/mocks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { createInventoryViewsServiceStartMock } from './services/inventory_views/inventory_views_service.mock'; import { createLogViewsServiceSetupMock, createLogViewsServiceStartMock, @@ -23,6 +24,7 @@ const createInfraSetupMock = () => { const createInfraStartMock = () => { const infraStartMock: jest.Mocked = { getMetricIndices: jest.fn(), + inventoryViews: createInventoryViewsServiceStartMock(), logViews: createLogViewsServiceStartMock(), }; return infraStartMock; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 7e7507c1d2e45..2c114bb75d6e5 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -20,8 +20,6 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants'; import { defaultLogViewsStaticConfig } from '../common/log_views'; import { publicConfigKeys } from '../common/plugin_config_types'; -import { inventoryViewSavedObjectType } from '../common/saved_objects/inventory_view'; -import { metricsExplorerViewSavedObjectType } from '../common/saved_objects/metrics_explorer_view'; import { configDeprecations, getInfraDeprecationsFactory } from './deprecations'; import { LOGS_FEATURE, METRICS_FEATURE } from './features'; import { initInfraServer } from './infra_server'; @@ -43,7 +41,12 @@ import { InfraBackendLibs, InfraDomainLibs } from './lib/infra_types'; import { makeGetMetricIndices } from './lib/metrics/make_get_metric_indices'; import { infraSourceConfigurationSavedObjectType, InfraSources } from './lib/sources'; import { InfraSourceStatus } from './lib/source_status'; -import { logViewSavedObjectType } from './saved_objects'; +import { + inventoryViewSavedObjectType, + logViewSavedObjectType, + metricsExplorerViewSavedObjectType, +} from './saved_objects'; +import { InventoryViewsService } from './services/inventory_views'; import { LogEntriesService } from './services/log_entries'; import { LogViewsService } from './services/log_views'; import { RulesService } from './services/rules'; @@ -117,6 +120,7 @@ export class InfraServerPlugin private logsRules: RulesService; private metricsRules: RulesService; + private inventoryViews: InventoryViewsService; private logViews: LogViewsService; constructor(context: PluginInitializerContext) { @@ -134,6 +138,7 @@ export class InfraServerPlugin this.logger.get('metricsRules') ); + this.inventoryViews = new InventoryViewsService(this.logger.get('inventoryViews')); this.logViews = new LogViewsService(this.logger.get('logViews')); } @@ -148,6 +153,7 @@ export class InfraServerPlugin sources, } ); + const inventoryViews = this.inventoryViews.setup(); const logViews = this.logViews.setup(); // register saved object types @@ -229,11 +235,17 @@ export class InfraServerPlugin return { defineInternalSourceConfiguration: sources.defineInternalSourceConfiguration.bind(sources), + inventoryViews, logViews, } as InfraPluginSetup; } start(core: CoreStart, plugins: InfraServerPluginStartDeps) { + const inventoryViews = this.inventoryViews.start({ + infraSources: this.libs.sources, + savedObjects: core.savedObjects, + }); + const logViews = this.logViews.start({ infraSources: this.libs.sources, savedObjects: core.savedObjects, @@ -247,6 +259,7 @@ export class InfraServerPlugin }); return { + inventoryViews, logViews, getMetricIndices: makeGetMetricIndices(this.libs.sources), }; diff --git a/x-pack/plugins/infra/server/routes/infra/lib/constants.ts b/x-pack/plugins/infra/server/routes/infra/lib/constants.ts index bc9131d1d52fc..f39eaafdd039e 100644 --- a/x-pack/plugins/infra/server/routes/infra/lib/constants.ts +++ b/x-pack/plugins/infra/server/routes/infra/lib/constants.ts @@ -24,6 +24,9 @@ export const METADATA_AGGREGATION: Record) => { + framework.registerRoute( + { + method: 'post', + path: INVENTORY_VIEW_URL, + validate: { + body: createValidationFunction(createInventoryViewRequestPayloadRT), + }, + }, + async (_requestContext, request, response) => { + const { body } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryView = await inventoryViewsClient.create(body.attributes); + + return response.custom({ + statusCode: 201, + body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }), + }); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/delete_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/delete_inventory_view.ts new file mode 100644 index 0000000000000..83ad61fc46c52 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/delete_inventory_view.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isBoom } from '@hapi/boom'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { + inventoryViewRequestParamsRT, + INVENTORY_VIEW_URL_ENTITY, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initDeleteInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'delete', + path: INVENTORY_VIEW_URL_ENTITY, + validate: { + params: createValidationFunction(inventoryViewRequestParamsRT), + }, + }, + async (_requestContext, request, response) => { + const { params } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + await inventoryViewsClient.delete(params.inventoryViewId); + + return response.noContent(); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/find_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/find_inventory_view.ts new file mode 100644 index 0000000000000..abdfc2f8749e4 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/find_inventory_view.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createValidationFunction } from '../../../common/runtime_types'; +import { + findInventoryViewResponsePayloadRT, + inventoryViewRequestQueryRT, + INVENTORY_VIEW_URL, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initFindInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'get', + path: INVENTORY_VIEW_URL, + validate: { + query: createValidationFunction(inventoryViewRequestQueryRT), + }, + }, + async (_requestContext, request, response) => { + const { query } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryViewsList = await inventoryViewsClient.find(query); + + return response.ok({ + body: findInventoryViewResponsePayloadRT.encode({ data: inventoryViewsList }), + }); + } catch (error) { + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/get_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/get_inventory_view.ts new file mode 100644 index 0000000000000..1a5f5adec136d --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/get_inventory_view.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isBoom } from '@hapi/boom'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { + inventoryViewResponsePayloadRT, + inventoryViewRequestQueryRT, + INVENTORY_VIEW_URL_ENTITY, + getInventoryViewRequestParamsRT, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initGetInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'get', + path: INVENTORY_VIEW_URL_ENTITY, + validate: { + params: createValidationFunction(getInventoryViewRequestParamsRT), + query: createValidationFunction(inventoryViewRequestQueryRT), + }, + }, + async (_requestContext, request, response) => { + const { params, query } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryView = await inventoryViewsClient.get(params.inventoryViewId, query); + + return response.ok({ + body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }), + }); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/index.ts b/x-pack/plugins/infra/server/routes/inventory_views/index.ts new file mode 100644 index 0000000000000..55cee58a8a464 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { InfraBackendLibs } from '../../lib/infra_types'; +import { initCreateInventoryViewRoute } from './create_inventory_view'; +import { initDeleteInventoryViewRoute } from './delete_inventory_view'; +import { initFindInventoryViewRoute } from './find_inventory_view'; +import { initGetInventoryViewRoute } from './get_inventory_view'; +import { initUpdateInventoryViewRoute } from './update_inventory_view'; + +export const initInventoryViewRoutes = ( + dependencies: Pick +) => { + initCreateInventoryViewRoute(dependencies); + initDeleteInventoryViewRoute(dependencies); + initFindInventoryViewRoute(dependencies); + initGetInventoryViewRoute(dependencies); + initUpdateInventoryViewRoute(dependencies); +}; diff --git a/x-pack/plugins/infra/server/routes/inventory_views/update_inventory_view.ts b/x-pack/plugins/infra/server/routes/inventory_views/update_inventory_view.ts new file mode 100644 index 0000000000000..d2b583437d177 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/inventory_views/update_inventory_view.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 { isBoom } from '@hapi/boom'; +import { createValidationFunction } from '../../../common/runtime_types'; +import { + inventoryViewRequestParamsRT, + inventoryViewRequestQueryRT, + inventoryViewResponsePayloadRT, + INVENTORY_VIEW_URL_ENTITY, + updateInventoryViewRequestPayloadRT, +} from '../../../common/http_api/latest'; +import type { InfraBackendLibs } from '../../lib/infra_types'; + +export const initUpdateInventoryViewRoute = ({ + framework, + getStartServices, +}: Pick) => { + framework.registerRoute( + { + method: 'put', + path: INVENTORY_VIEW_URL_ENTITY, + validate: { + params: createValidationFunction(inventoryViewRequestParamsRT), + query: createValidationFunction(inventoryViewRequestQueryRT), + body: createValidationFunction(updateInventoryViewRequestPayloadRT), + }, + }, + async (_requestContext, request, response) => { + const { body, params, query } = request; + const { inventoryViews } = (await getStartServices())[2]; + const inventoryViewsClient = inventoryViews.getScopedClient(request); + + try { + const inventoryView = await inventoryViewsClient.update( + params.inventoryViewId, + body.attributes, + query + ); + + return response.ok({ + body: inventoryViewResponsePayloadRT.encode({ data: inventoryView }), + }); + } catch (error) { + if (isBoom(error)) { + return response.customError({ + statusCode: error.output.statusCode, + body: { message: error.output.payload.message }, + }); + } + + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/infra/server/saved_objects/index.ts b/x-pack/plugins/infra/server/saved_objects/index.ts index bd7ecac5179a1..cf6906fc733f7 100644 --- a/x-pack/plugins/infra/server/saved_objects/index.ts +++ b/x-pack/plugins/infra/server/saved_objects/index.ts @@ -5,4 +5,6 @@ * 2.0. */ +export * from './inventory_view'; export * from './log_view'; +export * from './metrics_explorer_view'; diff --git a/x-pack/plugins/infra/server/saved_objects/inventory_view/index.ts b/x-pack/plugins/infra/server/saved_objects/inventory_view/index.ts new file mode 100644 index 0000000000000..458d3fa65c6a0 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/inventory_view/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + inventoryViewSavedObjectName, + inventoryViewSavedObjectType, +} from './inventory_view_saved_object'; +export { inventoryViewSavedObjectRT } from './types'; diff --git a/x-pack/plugins/infra/server/saved_objects/inventory_view/inventory_view_saved_object.ts b/x-pack/plugins/infra/server/saved_objects/inventory_view/inventory_view_saved_object.ts new file mode 100644 index 0000000000000..f9c4c4d354024 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/inventory_view/inventory_view_saved_object.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import type { SavedObject, SavedObjectsType } from '@kbn/core/server'; +import { inventoryViewSavedObjectRT } from './types'; + +export const inventoryViewSavedObjectName = 'inventory-view'; + +const getInventoryViewTitle = (savedObject: SavedObject) => + pipe( + inventoryViewSavedObjectRT.decode(savedObject), + fold( + () => `Inventory view [id=${savedObject.id}]`, + ({ attributes: { name } }) => name + ) + ); + +export const inventoryViewSavedObjectType: SavedObjectsType = { + name: inventoryViewSavedObjectName, + hidden: false, + namespaceType: 'single', + management: { + defaultSearchField: 'name', + displayName: 'inventory view', + getTitle: getInventoryViewTitle, + icon: 'metricsApp', + importableAndExportable: true, + }, + mappings: { + dynamic: false, + properties: {}, + }, +}; diff --git a/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts b/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts new file mode 100644 index 0000000000000..45e738f3920f1 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/inventory_view/types.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isoToEpochRt, nonEmptyStringRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; + +export const inventoryViewSavedObjectAttributesRT = rt.intersection([ + rt.strict({ + name: nonEmptyStringRt, + }), + rt.UnknownRecord, +]); + +export const inventoryViewSavedObjectRT = rt.intersection([ + rt.type({ + id: rt.string, + attributes: inventoryViewSavedObjectAttributesRT, + }), + rt.partial({ + version: rt.string, + updated_at: isoToEpochRt, + }), +]); diff --git a/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/index.ts b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/index.ts new file mode 100644 index 0000000000000..6f3f926319cf2 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { + metricsExplorerViewSavedObjectName, + metricsExplorerViewSavedObjectType, +} from './metrics_explorer_view_saved_object'; +export { metricsExplorerViewSavedObjectRT } from './types'; diff --git a/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/metrics_explorer_view_saved_object.ts b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/metrics_explorer_view_saved_object.ts new file mode 100644 index 0000000000000..ce47aa93951b6 --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/metrics_explorer_view_saved_object.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fold } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import type { SavedObject, SavedObjectsType } from '@kbn/core/server'; +import { metricsExplorerViewSavedObjectRT } from './types'; + +export const metricsExplorerViewSavedObjectName = 'metrics-explorer-view'; + +const getMetricsExplorerViewTitle = (savedObject: SavedObject) => + pipe( + metricsExplorerViewSavedObjectRT.decode(savedObject), + fold( + () => `Metrics explorer view [id=${savedObject.id}]`, + ({ attributes: { name } }) => name + ) + ); + +export const metricsExplorerViewSavedObjectType: SavedObjectsType = { + name: metricsExplorerViewSavedObjectName, + hidden: false, + namespaceType: 'single', + management: { + defaultSearchField: 'name', + displayName: 'metrics explorer view', + getTitle: getMetricsExplorerViewTitle, + icon: 'metricsApp', + importableAndExportable: true, + }, + mappings: { + dynamic: false, + properties: {}, + }, +}; diff --git a/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/types.ts b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/types.ts new file mode 100644 index 0000000000000..1168b2003994e --- /dev/null +++ b/x-pack/plugins/infra/server/saved_objects/metrics_explorer_view/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isoToEpochRt } from '@kbn/io-ts-utils'; +import * as rt from 'io-ts'; +import { metricsExplorerViewAttributesRT } from '../../../common/metrics_explorer_views'; + +export const metricsExplorerViewSavedObjectRT = rt.intersection([ + rt.type({ + id: rt.string, + attributes: metricsExplorerViewAttributesRT, + }), + rt.partial({ + version: rt.string, + updated_at: isoToEpochRt, + }), +]); diff --git a/x-pack/plugins/infra/server/services/inventory_views/index.ts b/x-pack/plugins/infra/server/services/inventory_views/index.ts new file mode 100644 index 0000000000000..1df6b8cd44814 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { InventoryViewsService } from './inventory_views_service'; +export { InventoryViewsClient } from './inventory_views_client'; +export type { + InventoryViewsServiceSetup, + InventoryViewsServiceStart, + InventoryViewsServiceStartDeps, +} from './types'; diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts new file mode 100644 index 0000000000000..9d832f8502104 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.mock.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { IInventoryViewsClient } from './types'; + +export const createInventoryViewsClientMock = (): jest.Mocked => ({ + delete: jest.fn(), + find: jest.fn(), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), +}); diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.test.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.test.ts new file mode 100644 index 0000000000000..5d5b253045de4 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.test.ts @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock } from '@kbn/logging-mocks'; +import { SavedObjectsClientContract } from '@kbn/core/server'; +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { InventoryViewAttributes } from '../../../common/inventory_views'; + +import { InfraSource } from '../../lib/sources'; +import { createInfraSourcesMock } from '../../lib/sources/mocks'; +import { inventoryViewSavedObjectName } from '../../saved_objects/inventory_view'; +import { InventoryViewsClient } from './inventory_views_client'; +import { createInventoryViewMock } from '../../../common/inventory_views/inventory_view.mock'; +import { + CreateInventoryViewAttributesRequestPayload, + UpdateInventoryViewAttributesRequestPayload, +} from '../../../common/http_api/latest'; + +describe('InventoryViewsClient class', () => { + const mockFindInventoryList = (savedObjectsClient: jest.Mocked) => { + const inventoryViewListMock = [ + createInventoryViewMock('0', { + isDefault: true, + } as InventoryViewAttributes), + createInventoryViewMock('default_id', { + name: 'Default view 2', + isStatic: false, + } as InventoryViewAttributes), + createInventoryViewMock('custom_id', { + name: 'Custom', + isStatic: false, + } as InventoryViewAttributes), + ]; + + savedObjectsClient.find.mockResolvedValue({ + total: 2, + saved_objects: inventoryViewListMock.slice(1).map((view) => ({ + ...view, + type: inventoryViewSavedObjectName, + score: 0, + references: [], + })), + per_page: 1000, + page: 1, + }); + + return inventoryViewListMock; + }; + + describe('.find', () => { + it('resolves the list of existing inventory views', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = + createInventoryViewsClient(); + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + const inventoryViewListMock = mockFindInventoryList(savedObjectsClient); + + const inventoryViewList = await inventoryViewsClient.find({}); + + expect(savedObjectsClient.find).toHaveBeenCalled(); + expect(inventoryViewList).toEqual(inventoryViewListMock); + }); + + it('always resolves at least the static inventory view', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = + createInventoryViewsClient(); + + const inventoryViewListMock = [ + createInventoryViewMock('0', { + isDefault: true, + } as InventoryViewAttributes), + ]; + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + savedObjectsClient.find.mockResolvedValue({ + total: 2, + saved_objects: [], + per_page: 1000, + page: 1, + }); + + const inventoryViewList = await inventoryViewsClient.find({}); + + expect(savedObjectsClient.find).toHaveBeenCalled(); + expect(inventoryViewList).toEqual(inventoryViewListMock); + }); + }); + + it('.get resolves the an inventory view by id', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = createInventoryViewsClient(); + + const inventoryViewMock = createInventoryViewMock('custom_id', { + name: 'Custom', + isDefault: false, + isStatic: false, + } as InventoryViewAttributes); + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + savedObjectsClient.get.mockResolvedValue({ + ...inventoryViewMock, + type: inventoryViewSavedObjectName, + references: [], + }); + + const inventoryView = await inventoryViewsClient.get('custom_id', {}); + + expect(savedObjectsClient.get).toHaveBeenCalled(); + expect(inventoryView).toEqual(inventoryViewMock); + }); + + describe('.create', () => { + it('generate a new inventory view', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + const inventoryViewMock = createInventoryViewMock('new_id', { + name: 'New view', + isStatic: false, + } as InventoryViewAttributes); + + mockFindInventoryList(savedObjectsClient); + + savedObjectsClient.create.mockResolvedValue({ + ...inventoryViewMock, + type: inventoryViewSavedObjectName, + references: [], + }); + + const inventoryView = await inventoryViewsClient.create({ + name: 'New view', + } as CreateInventoryViewAttributesRequestPayload); + + expect(savedObjectsClient.create).toHaveBeenCalled(); + expect(inventoryView).toEqual(inventoryViewMock); + }); + + it('throws an error when a conflicting name is given', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + mockFindInventoryList(savedObjectsClient); + + await expect( + async () => + await inventoryViewsClient.create({ + name: 'Custom', + } as CreateInventoryViewAttributesRequestPayload) + ).rejects.toThrow('A view with that name already exists.'); + }); + }); + + describe('.update', () => { + it('update an existing inventory view by id', async () => { + const { inventoryViewsClient, infraSources, savedObjectsClient } = + createInventoryViewsClient(); + + const inventoryViews = mockFindInventoryList(savedObjectsClient); + + const inventoryViewMock = { + ...inventoryViews[1], + attributes: { + ...inventoryViews[1].attributes, + name: 'New name', + }, + }; + + infraSources.getSourceConfiguration.mockResolvedValue(basicTestSourceConfiguration); + + savedObjectsClient.update.mockResolvedValue({ + ...inventoryViewMock, + type: inventoryViewSavedObjectName, + references: [], + }); + + const inventoryView = await inventoryViewsClient.update( + 'default_id', + { + name: 'New name', + } as UpdateInventoryViewAttributesRequestPayload, + {} + ); + + expect(savedObjectsClient.update).toHaveBeenCalled(); + expect(inventoryView).toEqual(inventoryViewMock); + }); + + it('throws an error when a conflicting name is given', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + mockFindInventoryList(savedObjectsClient); + + await expect( + async () => + await inventoryViewsClient.update( + 'default_id', + { + name: 'Custom', + } as UpdateInventoryViewAttributesRequestPayload, + {} + ) + ).rejects.toThrow('A view with that name already exists.'); + }); + }); + + it('.delete removes an inventory view by id', async () => { + const { inventoryViewsClient, savedObjectsClient } = createInventoryViewsClient(); + + savedObjectsClient.delete.mockResolvedValue({}); + + const inventoryView = await inventoryViewsClient.delete('custom_id'); + + expect(savedObjectsClient.delete).toHaveBeenCalled(); + expect(inventoryView).toEqual({}); + }); +}); + +const createInventoryViewsClient = () => { + const logger = loggerMock.create(); + const savedObjectsClient = savedObjectsClientMock.create(); + const infraSources = createInfraSourcesMock(); + + const inventoryViewsClient = new InventoryViewsClient(logger, savedObjectsClient, infraSources); + + return { + infraSources, + inventoryViewsClient, + savedObjectsClient, + }; +}; + +const basicTestSourceConfiguration: InfraSource = { + id: 'ID', + origin: 'stored', + configuration: { + name: 'NAME', + description: 'DESCRIPTION', + logIndices: { + type: 'index_pattern', + indexPatternId: 'INDEX_PATTERN_ID', + }, + logColumns: [], + fields: { + message: [], + }, + metricAlias: 'METRIC_ALIAS', + inventoryDefaultView: '0', + metricsExplorerDefaultView: 'METRICS_EXPLORER_DEFAULT_VIEW', + anomalyThreshold: 0, + }, +}; diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts new file mode 100644 index 0000000000000..55a8df1024a6e --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_client.ts @@ -0,0 +1,199 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + Logger, + SavedObject, + SavedObjectsClientContract, + SavedObjectsUpdateResponse, +} from '@kbn/core/server'; +import Boom from '@hapi/boom'; +import { + staticInventoryViewAttributes, + staticInventoryViewId, +} from '../../../common/inventory_views'; +import type { + CreateInventoryViewAttributesRequestPayload, + InventoryViewRequestQuery, +} from '../../../common/http_api/latest'; +import type { InventoryView, InventoryViewAttributes } from '../../../common/inventory_views'; +import { decodeOrThrow } from '../../../common/runtime_types'; +import type { IInfraSources } from '../../lib/sources'; +import { inventoryViewSavedObjectName } from '../../saved_objects/inventory_view'; +import { inventoryViewSavedObjectRT } from '../../saved_objects/inventory_view/types'; +import type { IInventoryViewsClient } from './types'; + +export class InventoryViewsClient implements IInventoryViewsClient { + constructor( + private readonly logger: Logger, + private readonly savedObjectsClient: SavedObjectsClientContract, + private readonly infraSources: IInfraSources + ) {} + + static STATIC_VIEW_ID = '0'; + + public async find(query: InventoryViewRequestQuery): Promise { + this.logger.debug('Trying to load inventory views ...'); + + const sourceId = query.sourceId ?? 'default'; + + const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([ + this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), + this.savedObjectsClient.find({ + type: inventoryViewSavedObjectName, + perPage: 1000, // Fetch 1 page by default with a max of 1000 results + }), + ]); + + const defaultView = InventoryViewsClient.createStaticView( + sourceConfiguration.configuration.inventoryDefaultView + ); + const views = inventoryViewSavedObject.saved_objects.map((savedObject) => + this.mapSavedObjectToInventoryView( + savedObject, + sourceConfiguration.configuration.inventoryDefaultView + ) + ); + + const inventoryViews = [defaultView, ...views]; + + const sortedInventoryViews = this.moveDefaultViewOnTop(inventoryViews); + + return sortedInventoryViews; + } + + public async get( + inventoryViewId: string, + query: InventoryViewRequestQuery + ): Promise { + this.logger.debug(`Trying to load inventory view with id ${inventoryViewId} ...`); + + const sourceId = query.sourceId ?? 'default'; + + // Handle the case where the requested resource is the static inventory view + if (inventoryViewId === InventoryViewsClient.STATIC_VIEW_ID) { + const sourceConfiguration = await this.infraSources.getSourceConfiguration( + this.savedObjectsClient, + sourceId + ); + + return InventoryViewsClient.createStaticView( + sourceConfiguration.configuration.inventoryDefaultView + ); + } + + const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([ + this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), + this.savedObjectsClient.get(inventoryViewSavedObjectName, inventoryViewId), + ]); + + return this.mapSavedObjectToInventoryView( + inventoryViewSavedObject, + sourceConfiguration.configuration.inventoryDefaultView + ); + } + + public async create( + attributes: CreateInventoryViewAttributesRequestPayload + ): Promise { + this.logger.debug(`Trying to create inventory view ...`); + + // Validate there is not a view with the same name + await this.assertNameConflict(attributes.name); + + const inventoryViewSavedObject = await this.savedObjectsClient.create( + inventoryViewSavedObjectName, + attributes + ); + + return this.mapSavedObjectToInventoryView(inventoryViewSavedObject); + } + + public async update( + inventoryViewId: string, + attributes: CreateInventoryViewAttributesRequestPayload, + query: InventoryViewRequestQuery + ): Promise { + this.logger.debug(`Trying to update inventory view with id "${inventoryViewId}"...`); + + // Validate there is not a view with the same name + await this.assertNameConflict(attributes.name, [inventoryViewId]); + + const sourceId = query.sourceId ?? 'default'; + + const [sourceConfiguration, inventoryViewSavedObject] = await Promise.all([ + this.infraSources.getSourceConfiguration(this.savedObjectsClient, sourceId), + this.savedObjectsClient.update(inventoryViewSavedObjectName, inventoryViewId, attributes), + ]); + + return this.mapSavedObjectToInventoryView( + inventoryViewSavedObject, + sourceConfiguration.configuration.inventoryDefaultView + ); + } + + public delete(inventoryViewId: string): Promise<{}> { + this.logger.debug(`Trying to delete inventory view with id ${inventoryViewId} ...`); + + return this.savedObjectsClient.delete(inventoryViewSavedObjectName, inventoryViewId); + } + + private mapSavedObjectToInventoryView( + savedObject: SavedObject | SavedObjectsUpdateResponse, + defaultViewId?: string + ) { + const inventoryViewSavedObject = decodeOrThrow(inventoryViewSavedObjectRT)(savedObject); + + return { + id: inventoryViewSavedObject.id, + version: inventoryViewSavedObject.version, + updatedAt: inventoryViewSavedObject.updated_at, + attributes: { + ...inventoryViewSavedObject.attributes, + isDefault: inventoryViewSavedObject.id === defaultViewId, + isStatic: false, + }, + }; + } + + private moveDefaultViewOnTop(views: InventoryView[]) { + const defaultViewPosition = views.findIndex((view) => view.attributes.isDefault); + + if (defaultViewPosition !== -1) { + const element = views.splice(defaultViewPosition, 1)[0]; + views.unshift(element); + } + + return views; + } + + /** + * We want to control conflicting names on the views + */ + private async assertNameConflict(name: string, whitelist: string[] = []) { + const results = await this.savedObjectsClient.find({ + type: inventoryViewSavedObjectName, + perPage: 1000, + }); + + const hasConflict = [InventoryViewsClient.createStaticView(), ...results.saved_objects].some( + (obj) => !whitelist.includes(obj.id) && obj.attributes.name === name + ); + + if (hasConflict) { + throw Boom.conflict('A view with that name already exists.'); + } + } + + private static createStaticView = (defaultViewId?: string): InventoryView => ({ + id: staticInventoryViewId, + attributes: { + ...staticInventoryViewAttributes, + isDefault: defaultViewId === InventoryViewsClient.STATIC_VIEW_ID, + }, + }); +} diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.mock.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.mock.ts new file mode 100644 index 0000000000000..cb3e85643303c --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.mock.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 { createInventoryViewsClientMock } from './inventory_views_client.mock'; +import type { InventoryViewsServiceSetup, InventoryViewsServiceStart } from './types'; + +export const createInventoryViewsServiceSetupMock = + (): jest.Mocked => {}; + +export const createInventoryViewsServiceStartMock = + (): jest.Mocked => ({ + getClient: jest.fn((_savedObjectsClient: any) => createInventoryViewsClientMock()), + getScopedClient: jest.fn((_request: any) => createInventoryViewsClientMock()), + }); diff --git a/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.ts b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.ts new file mode 100644 index 0000000000000..3f51f0e65b29c --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/inventory_views_service.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server'; +import { InventoryViewsClient } from './inventory_views_client'; +import type { + InventoryViewsServiceSetup, + InventoryViewsServiceStart, + InventoryViewsServiceStartDeps, +} from './types'; + +export class InventoryViewsService { + constructor(private readonly logger: Logger) {} + + public setup(): InventoryViewsServiceSetup {} + + public start({ + infraSources, + savedObjects, + }: InventoryViewsServiceStartDeps): InventoryViewsServiceStart { + const { logger } = this; + + return { + getClient(savedObjectsClient: SavedObjectsClientContract) { + return new InventoryViewsClient(logger, savedObjectsClient, infraSources); + }, + + getScopedClient(request: KibanaRequest) { + const savedObjectsClient = savedObjects.getScopedClient(request); + + return this.getClient(savedObjectsClient); + }, + }; + } +} diff --git a/x-pack/plugins/infra/server/services/inventory_views/types.ts b/x-pack/plugins/infra/server/services/inventory_views/types.ts new file mode 100644 index 0000000000000..3e023b77af6c2 --- /dev/null +++ b/x-pack/plugins/infra/server/services/inventory_views/types.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + KibanaRequest, + SavedObjectsClientContract, + SavedObjectsServiceStart, +} from '@kbn/core/server'; +import type { + CreateInventoryViewAttributesRequestPayload, + InventoryViewRequestQuery, + UpdateInventoryViewAttributesRequestPayload, +} from '../../../common/http_api/latest'; +import type { InventoryView } from '../../../common/inventory_views'; +import type { InfraSources } from '../../lib/sources'; + +export interface InventoryViewsServiceStartDeps { + infraSources: InfraSources; + savedObjects: SavedObjectsServiceStart; +} + +export type InventoryViewsServiceSetup = void; + +export interface InventoryViewsServiceStart { + getClient(savedObjectsClient: SavedObjectsClientContract): IInventoryViewsClient; + getScopedClient(request: KibanaRequest): IInventoryViewsClient; +} + +export interface IInventoryViewsClient { + delete(inventoryViewId: string): Promise<{}>; + find(query: InventoryViewRequestQuery): Promise; + get(inventoryViewId: string, query: InventoryViewRequestQuery): Promise; + create( + inventoryViewAttributes: CreateInventoryViewAttributesRequestPayload + ): Promise; + update( + inventoryViewId: string, + inventoryViewAttributes: UpdateInventoryViewAttributesRequestPayload, + query: InventoryViewRequestQuery + ): Promise; +} diff --git a/x-pack/plugins/infra/server/types.ts b/x-pack/plugins/infra/server/types.ts index 108575c0f8324..c415103d2256d 100644 --- a/x-pack/plugins/infra/server/types.ts +++ b/x-pack/plugins/infra/server/types.ts @@ -14,6 +14,7 @@ import type { SearchRequestHandlerContext } from '@kbn/data-plugin/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; import type { InfraStaticSourceConfiguration } from '../common/source_configuration/source_configuration'; import { InfraServerPluginStartDeps } from './lib/adapters/framework'; +import { InventoryViewsServiceStart } from './services/inventory_views'; import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views/types'; export type { InfraConfig } from '../common/plugin_config_types'; @@ -30,6 +31,7 @@ export interface InfraPluginSetup { } export interface InfraPluginStart { + inventoryViews: InventoryViewsServiceStart; logViews: LogViewsServiceStart; getMetricIndices: ( savedObjectsClient: SavedObjectsClientContract, diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index dd0632788e847..76cbce409ce2f 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -62,7 +62,7 @@ "@kbn/discover-plugin", "@kbn/observability-alert-details", "@kbn/observability-shared-plugin", - "@kbn/ui-theme" + "@kbn/ui-theme", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/lists/public/exceptions/api.test.ts b/x-pack/plugins/lists/public/exceptions/api.test.ts index 95675fe96aee8..a0f2bf9c21fe4 100644 --- a/x-pack/plugins/lists/public/exceptions/api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/api.test.ts @@ -11,6 +11,7 @@ import { addExceptionListItem, deleteExceptionListById, deleteExceptionListItemById, + duplicateExceptionList, exportExceptionList, fetchExceptionListById, fetchExceptionListItemById, @@ -728,4 +729,30 @@ describe('Exceptions Lists API', () => { expect(exceptionResponse).toEqual(blob); }); }); + + describe('#duplicateExceptionList', () => { + beforeEach(() => { + httpMock.fetch.mockResolvedValue(getExceptionListSchemaMock()); + }); + + test('it invokes "duplicateExceptionList" with expected url and body values', async () => { + await duplicateExceptionList({ + http: httpMock, + includeExpiredExceptions: false, + listId: 'my_list', + namespaceType: 'single', + signal: abortCtrl.signal, + }); + + expect(httpMock.fetch).toHaveBeenCalledWith('/api/exception_lists/_duplicate', { + method: 'POST', + query: { + include_expired_exceptions: false, + list_id: 'my_list', + namespace_type: 'single', + }, + signal: abortCtrl.signal, + }); + }); + }); }); diff --git a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts index bf10fb57f1a5c..55c8ebde7cebd 100644 --- a/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts +++ b/x-pack/plugins/lists/public/exceptions/hooks/use_api.test.ts @@ -12,6 +12,7 @@ import type { AddExceptionListItemProps, ApiCallByIdProps, ApiCallByListIdProps, + DuplicateExceptionListProps, UpdateExceptionListItemProps, } from '@kbn/securitysolution-io-ts-list-types'; import { coreMock } from '@kbn/core/public/mocks'; @@ -462,4 +463,61 @@ describe('useApi', () => { }); }); }); + + describe('duplicateExceptionList', () => { + test('it invokes "onSuccess" when duplication does not throw', async () => { + const onSuccessMock = jest.fn(); + const spyOnDuplicateExceptionList = jest + .spyOn(api, 'duplicateExceptionList') + .mockResolvedValue(getExceptionListSchemaMock()); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.duplicateExceptionList({ + includeExpiredExceptions: false, + listId: 'my_list', + namespaceType: 'single', + onError: jest.fn(), + onSuccess: onSuccessMock, + }); + + const expected: DuplicateExceptionListProps = { + http: mockKibanaHttpService, + includeExpiredExceptions: false, + listId: 'my_list', + namespaceType: 'single', + signal: new AbortController().signal, + }; + + expect(spyOnDuplicateExceptionList).toHaveBeenCalledWith(expected); + expect(onSuccessMock).toHaveBeenCalled(); + }); + }); + + test('invokes "onError" callback if "duplicateExceptionList" fails', async () => { + const mockError = new Error('failed to duplicate item'); + jest.spyOn(api, 'duplicateExceptionList').mockRejectedValue(mockError); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useApi(mockKibanaHttpService) + ); + await waitForNextUpdate(); + + await result.current.duplicateExceptionList({ + includeExpiredExceptions: false, + listId: 'my_list', + namespaceType: 'single', + onError: onErrorMock, + onSuccess: jest.fn(), + }); + + expect(onErrorMock).toHaveBeenCalledWith(mockError); + }); + }); + }); }); diff --git a/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts b/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts new file mode 100644 index 0000000000000..2724215971aae --- /dev/null +++ b/x-pack/plugins/lists/server/routes/duplicate_exception_list_route.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { transformError } from '@kbn/securitysolution-es-utils'; +import { + DuplicateExceptionListQuerySchemaDecoded, + duplicateExceptionListQuerySchema, + exceptionListSchema, +} from '@kbn/securitysolution-io-ts-list-types'; +import { validate } from '@kbn/securitysolution-io-ts-utils'; +import { EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; + +import type { ListsPluginRouter } from '../types'; + +import { buildRouteValidation, buildSiemResponse, getExceptionListClient } from './utils'; + +export const duplicateExceptionsRoute = (router: ListsPluginRouter): void => { + router.post( + { + options: { + tags: ['access:lists-all'], + }, + path: `${EXCEPTION_LIST_URL}/_duplicate`, + validate: { + query: buildRouteValidation< + typeof duplicateExceptionListQuerySchema, + DuplicateExceptionListQuerySchemaDecoded + >(duplicateExceptionListQuerySchema), + }, + }, + async (context, request, response) => { + const siemResponse = buildSiemResponse(response); + + try { + const { + list_id: listId, + namespace_type: namespaceType, + include_expired_exceptions: includeExpiredExceptionsString, + } = request.query; + + const exceptionListsClient = await getExceptionListClient(context); + + // fetch list container + const listToDuplicate = await exceptionListsClient.getExceptionList({ + id: undefined, + listId, + namespaceType, + }); + + if (listToDuplicate == null) { + return siemResponse.error({ + body: `exception list id: "${listId}" does not exist`, + statusCode: 404, + }); + } + + // Defaults to including expired exceptions if query param is not present + const includeExpiredExceptions = + includeExpiredExceptionsString !== undefined + ? includeExpiredExceptionsString === 'true' + : true; + const duplicatedList = await exceptionListsClient.duplicateExceptionListAndItems({ + includeExpiredExceptions, + list: listToDuplicate, + namespaceType, + }); + + if (duplicatedList == null) { + return siemResponse.error({ + body: `unable to duplicate exception list with list_id: ${listId} - action not allowed`, + statusCode: 405, + }); + } + + const [validated, errors] = validate(duplicatedList, exceptionListSchema); + if (errors != null) { + return siemResponse.error({ body: errors, statusCode: 500 }); + } else { + return response.ok({ body: validated ?? {} }); + } + } catch (err) { + const error = transformError(err); + return siemResponse.error({ + body: error.message, + statusCode: error.statusCode, + }); + } + } + ); +}; diff --git a/x-pack/plugins/lists/server/routes/index.ts b/x-pack/plugins/lists/server/routes/index.ts index 851ed971c9b09..6347d564981bf 100644 --- a/x-pack/plugins/lists/server/routes/index.ts +++ b/x-pack/plugins/lists/server/routes/index.ts @@ -18,6 +18,7 @@ export * from './delete_exception_list_item_route'; export * from './delete_list_index_route'; export * from './delete_list_item_route'; export * from './delete_list_route'; +export * from './duplicate_exception_list_route'; export * from './export_exception_list_route'; export * from './export_list_item_route'; export * from './find_endpoint_list_item_route'; diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts index 4649a82a8e4a1..7fd87c72765bd 100644 --- a/x-pack/plugins/lists/server/routes/init_routes.ts +++ b/x-pack/plugins/lists/server/routes/init_routes.ts @@ -22,6 +22,7 @@ import { deleteListIndexRoute, deleteListItemRoute, deleteListRoute, + duplicateExceptionsRoute, exportExceptionsRoute, exportListItemRoute, findEndpointListItemRoute, @@ -87,6 +88,7 @@ export const initRoutes = (router: ListsPluginRouter, config: ConfigType): void updateExceptionListRoute(router); deleteExceptionListRoute(router); findExceptionListRoute(router); + duplicateExceptionsRoute(router); // exception list items createExceptionListItemRoute(router); diff --git a/x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.test.ts b/x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.test.ts new file mode 100644 index 0000000000000..562c6ac674beb --- /dev/null +++ b/x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock } from '@kbn/core/server/mocks'; + +import { + getDetectionsExceptionListSchemaMock, + getTrustedAppsListSchemaMock, +} from '../../../common/schemas/response/exception_list_schema.mock'; +import { getExceptionListItemSchemaMock } from '../../../common/schemas/response/exception_list_item_schema.mock'; + +import { findExceptionListsItemPointInTimeFinder } from './find_exception_list_items_point_in_time_finder'; +import { duplicateExceptionListAndItems } from './duplicate_exception_list'; +import { getExceptionList } from './get_exception_list'; +import { createExceptionList } from './create_exception_list'; + +jest.mock('./get_exception_list'); +jest.mock('./create_exception_list'); +jest.mock('./bulk_create_exception_list_items'); +jest.mock('./find_exception_list_items_point_in_time_finder'); + +const mockCurrentTime = new Date('2023-02-01T10:20:30Z'); + +describe('duplicateExceptionListAndItems', () => { + beforeEach(() => { + jest.useFakeTimers(); + jest.setSystemTime(mockCurrentTime); + }); + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + test('should return null exception list is not of type "detection" or "rule_default"', async () => { + (getExceptionList as jest.Mock).mockResolvedValue(getTrustedAppsListSchemaMock()); + + const result = await duplicateExceptionListAndItems({ + includeExpiredExceptions: true, + list: getTrustedAppsListSchemaMock(), + namespaceType: 'single', + savedObjectsClient: savedObjectsClientMock.create(), + user: 'test-user', + }); + + expect(result).toBeNull(); + }); + + test('should duplicate a list with expired exceptions', async () => { + (getExceptionList as jest.Mock).mockResolvedValue(getDetectionsExceptionListSchemaMock()); + (createExceptionList as jest.Mock).mockResolvedValue({ + ...getDetectionsExceptionListSchemaMock(), + list_id: 'exception_list_id_dupe', + name: 'Test [Duplicate]', + }); + (findExceptionListsItemPointInTimeFinder as jest.Mock).mockImplementationOnce( + ({ executeFunctionOnStream }) => { + executeFunctionOnStream({ data: [getExceptionListItemSchemaMock()] }); + } + ); + + await duplicateExceptionListAndItems({ + includeExpiredExceptions: true, + list: getDetectionsExceptionListSchemaMock(), + namespaceType: 'single', + savedObjectsClient: savedObjectsClientMock.create(), + user: 'test-user', + }); + + expect(findExceptionListsItemPointInTimeFinder).toHaveBeenCalledWith({ + executeFunctionOnStream: expect.any(Function), + filter: [], + listId: ['exception_list_id'], + maxSize: 10000, + namespaceType: ['single'], + perPage: undefined, + savedObjectsClient: expect.any(Object), + sortField: undefined, + sortOrder: undefined, + }); + }); + + test('should duplicate a list without expired exceptions', async () => { + (getExceptionList as jest.Mock).mockResolvedValue(getDetectionsExceptionListSchemaMock()); + (createExceptionList as jest.Mock).mockResolvedValue({ + ...getDetectionsExceptionListSchemaMock(), + list_id: 'exception_list_id_dupe', + name: 'Test [Duplicate]', + }); + (findExceptionListsItemPointInTimeFinder as jest.Mock).mockImplementationOnce( + ({ executeFunctionOnStream }) => { + executeFunctionOnStream({ data: [getExceptionListItemSchemaMock()] }); + } + ); + + await duplicateExceptionListAndItems({ + includeExpiredExceptions: false, + list: getDetectionsExceptionListSchemaMock(), + namespaceType: 'single', + savedObjectsClient: savedObjectsClientMock.create(), + user: 'test-user', + }); + + expect(findExceptionListsItemPointInTimeFinder).toHaveBeenCalledWith({ + executeFunctionOnStream: expect.any(Function), + filter: [ + '(exception-list.attributes.expire_time > "2023-02-01T10:20:30.000Z" OR NOT exception-list.attributes.expire_time: *)', + ], + listId: ['exception_list_id'], + maxSize: 10000, + namespaceType: ['single'], + perPage: undefined, + savedObjectsClient: expect.any(Object), + sortField: undefined, + sortOrder: undefined, + }); + }); +}); diff --git a/x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.ts b/x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.ts index 4d4c8a07b455e..0bd17f8c39e2e 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/duplicate_exception_list.ts @@ -12,13 +12,12 @@ import { ExceptionListSchema, ExceptionListTypeEnum, FoundExceptionListItemSchema, - ListId, NamespaceType, } from '@kbn/securitysolution-io-ts-list-types'; +import { getSavedObjectType } from '@kbn/securitysolution-list-utils'; import { findExceptionListsItemPointInTimeFinder } from './find_exception_list_items_point_in_time_finder'; import { bulkCreateExceptionListItems } from './bulk_create_exception_list_items'; -import { getExceptionList } from './get_exception_list'; import { createExceptionList } from './create_exception_list'; const LISTS_ABLE_TO_DUPLICATE = [ @@ -26,48 +25,38 @@ const LISTS_ABLE_TO_DUPLICATE = [ ExceptionListTypeEnum.RULE_DEFAULT.toString(), ]; -interface CreateExceptionListOptions { - listId: ListId; +interface DuplicateExceptionListOptions { + list: ExceptionListSchema; savedObjectsClient: SavedObjectsClientContract; namespaceType: NamespaceType; user: string; + includeExpiredExceptions: boolean; } export const duplicateExceptionListAndItems = async ({ - listId, + includeExpiredExceptions, + list, savedObjectsClient, namespaceType, user, -}: CreateExceptionListOptions): Promise => { +}: DuplicateExceptionListOptions): Promise => { // Generate a new static listId const newListId = uuidv4(); - // fetch list container - const listToDuplicate = await getExceptionList({ - id: undefined, - listId, - namespaceType, - savedObjectsClient, - }); - - if (listToDuplicate == null) { - throw new Error(`Exception list to duplicat of list_id:${listId} not found.`); - } - - if (!LISTS_ABLE_TO_DUPLICATE.includes(listToDuplicate.type)) { - throw new Error(`Exception list of type:${listToDuplicate.type} cannot be duplicated.`); + if (!LISTS_ABLE_TO_DUPLICATE.includes(list.type)) { + return null; } const newlyCreatedList = await createExceptionList({ - description: listToDuplicate.description, - immutable: listToDuplicate.immutable, + description: list.description, + immutable: list.immutable, listId: newListId, - meta: listToDuplicate.meta, - name: listToDuplicate.name, - namespaceType: listToDuplicate.namespace_type, + meta: list.meta, + name: `${list.name} [Duplicate]`, + namespaceType: list.namespace_type, savedObjectsClient, - tags: listToDuplicate.tags, - type: listToDuplicate.type, + tags: list.tags, + type: list.type, user, version: 1, }); @@ -96,10 +85,16 @@ export const duplicateExceptionListAndItems = async ({ }); itemsToBeDuplicated = [...itemsToBeDuplicated, ...transformedItems]; }; + const savedObjectPrefix = getSavedObjectType({ namespaceType }); + const filter = includeExpiredExceptions + ? [] + : [ + `(${savedObjectPrefix}.attributes.expire_time > "${new Date().toISOString()}" OR NOT ${savedObjectPrefix}.attributes.expire_time: *)`, + ]; await findExceptionListsItemPointInTimeFinder({ executeFunctionOnStream, - filter: [], - listId: [listId], + filter, + listId: [list.list_id], maxSize: 10000, namespaceType: [namespaceType], perPage: undefined, diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts index db9c62ae5b377..484353d30e346 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts @@ -317,17 +317,20 @@ export class ExceptionListClient { /** * Create the Trusted Apps Agnostic list if it does not yet exist (`null` is returned if it does exist) - * @param options.listId the "list_id" of the exception list + * @param options.list the "list" to be duplicated * @param options.namespaceType saved object namespace (single | agnostic) + * @param options.includeExpiredExceptions include or exclude expired TTL exception items * @returns The exception list schema or null if it does not exist */ public duplicateExceptionListAndItems = async ({ - listId, + list, namespaceType, + includeExpiredExceptions, }: DuplicateExceptionListOptions): Promise => { const { savedObjectsClient, user } = this; return duplicateExceptionListAndItems({ - listId, + includeExpiredExceptions, + list, namespaceType, savedObjectsClient, user, @@ -1051,6 +1054,7 @@ export class ExceptionListClient { * @param options.listId the "list_id" of an exception list * @param options.id the "id" of an exception list * @param options.namespaceType saved object namespace (single | agnostic) + * @param options.includeExpiredExceptions include or exclude expired TTL exception items * @returns the ndjson of the list and items to export or null if none exists */ public exportExceptionListAndItems = async ({ diff --git a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts index b93de1413e4db..14c75b84a1367 100644 --- a/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts +++ b/x-pack/plugins/lists/server/services/exception_lists/exception_list_client_types.ts @@ -19,6 +19,7 @@ import type { EntriesArray, ExceptionListItemType, ExceptionListItemTypeOrUndefined, + ExceptionListSchema, ExceptionListType, ExceptionListTypeOrUndefined, ExpireTimeOrUndefined, @@ -295,10 +296,12 @@ export interface CreateEndpointListItemOptions { * {@link ExceptionListClient.duplicateExceptionListAndItems} */ export interface DuplicateExceptionListOptions { - /** The single list id to do the search against */ - listId: ListId; + /** The list to be duplicated */ + list: ExceptionListSchema; /** saved object namespace (single | agnostic) */ namespaceType: NamespaceType; + /** determines whether exception items with an expired TTL are included in duplication */ + includeExpiredExceptions: boolean; } /** diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 6c8f719bf2886..8f5ad4869ebe7 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -188,10 +188,6 @@ export const GEOCENTROID_AGG_NAME = 'gridCentroid'; export const TOP_TERM_PERCENTAGE_SUFFIX = '__percentage'; export const DEFAULT_PERCENTILE = 50; -export const COUNT_PROP_LABEL = i18n.translate('xpack.maps.aggs.defaultCountLabel', { - defaultMessage: 'count', -}); - export const COUNT_PROP_NAME = 'doc_count'; export enum STYLE_TYPE { @@ -341,6 +337,11 @@ export enum WIZARD_ID { TMS_LAYER = 'tmsLayer', } +export enum MASK_OPERATOR { + ABOVE = 'ABOVE', + BELOW = 'BELOW', +} + // Maplibre does not provide any feedback when rendering is complete. // Workaround is hard-coded timeout period. export const RENDER_TIMEOUT = 1000; diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index b862558d6a215..c76bc8f6a6e17 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -13,6 +13,7 @@ import { SortDirection } from '@kbn/data-plugin/common/search'; import { AGG_TYPE, GRID_RESOLUTION, + MASK_OPERATOR, RENDER_AS, SCALING_TYPES, MVT_FIELD_TYPE, @@ -49,6 +50,10 @@ export type AbstractESSourceDescriptor = AbstractSourceDescriptor & { type AbstractAggDescriptor = { type: AGG_TYPE; label?: string; + mask?: { + operator: MASK_OPERATOR; + value: number; + }; }; export type CountAggDescriptor = AbstractAggDescriptor & { diff --git a/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts b/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts index d881accb1e42f..6db0d32dbd551 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/agg_field_types.ts @@ -9,14 +9,17 @@ import { DataView } from '@kbn/data-plugin/common'; import { IField } from '../field'; import { IESAggSource } from '../../sources/es_agg_source'; import { FIELD_ORIGIN } from '../../../../common/constants'; +import { AggDescriptor } from '../../../../common/descriptor_types'; export interface IESAggField extends IField { getValueAggDsl(indexPattern: DataView): unknown | null; getBucketCount(): number; + getMask(): AggDescriptor['mask'] | undefined; } export interface CountAggFieldParams { label?: string; source: IESAggSource; origin: FIELD_ORIGIN; + mask?: AggDescriptor['mask']; } diff --git a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts index 03ade37c3dbec..140f605832fca 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/count_agg_field.ts @@ -14,7 +14,7 @@ import { DataView } from '@kbn/data-plugin/common'; import { IESAggSource } from '../../sources/es_agg_source'; import { IVectorSource } from '../../sources/vector_source'; import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; -import { TileMetaFeature } from '../../../../common/descriptor_types'; +import { AggDescriptor, TileMetaFeature } from '../../../../common/descriptor_types'; import { ITooltipProperty, TooltipProperty } from '../../tooltips/tooltip_property'; import { ESAggTooltipProperty } from '../../tooltips/es_agg_tooltip_property'; import { IESAggField, CountAggFieldParams } from './agg_field_types'; @@ -25,11 +25,13 @@ export class CountAggField implements IESAggField { protected readonly _source: IESAggSource; private readonly _origin: FIELD_ORIGIN; protected readonly _label?: string; + protected readonly _mask?: AggDescriptor['mask']; - constructor({ label, source, origin }: CountAggFieldParams) { + constructor({ label, source, origin, mask }: CountAggFieldParams) { this._source = source; this._origin = origin; this._label = label; + this._mask = mask; } supportsFieldMetaFromEs(): boolean { @@ -131,4 +133,8 @@ export class CountAggField implements IESAggField { pluckRangeFromTileMetaFeature(metaFeature: TileMetaFeature) { return getAggRange(metaFeature, '_count'); } + + getMask() { + return this._mask; + } } diff --git a/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts index 7e43a2a63658c..9db4e481b9963 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/es_agg_factory.ts @@ -26,6 +26,7 @@ export function esAggFieldsFactory( label: aggDescriptor.label, source, origin, + mask: aggDescriptor.mask, }); } else if (aggDescriptor.type === AGG_TYPE.PERCENTILE) { aggField = new PercentileAggField({ @@ -40,6 +41,7 @@ export function esAggFieldsFactory( : DEFAULT_PERCENTILE, source, origin, + mask: aggDescriptor.mask, }); } else { aggField = new AggField({ @@ -51,6 +53,7 @@ export function esAggFieldsFactory( aggType: aggDescriptor.type, source, origin, + mask: aggDescriptor.mask, }); } diff --git a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts index c6a19f448390a..568fad59e058b 100644 --- a/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/agg/top_term_percentage_field.ts @@ -105,4 +105,8 @@ export class TopTermPercentageField implements IESAggField { pluckRangeFromTileMetaFeature(metaFeature: TileMetaFeature) { return null; } + + getMask() { + return undefined; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index 0421fc3b087b5..69988f0e8a6cb 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Map as MbMap, VectorTileSource } from '@kbn/mapbox-gl'; +import type { FilterSpecification, Map as MbMap, VectorTileSource } from '@kbn/mapbox-gl'; import { AbstractLayer } from '../layer'; import { HeatmapStyle } from '../../styles/heatmap/heatmap_style'; import { LAYER_TYPE } from '../../../../common/constants'; @@ -21,6 +21,7 @@ import { DataRequestContext } from '../../../actions'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { IMvtVectorSource } from '../../sources/vector_source'; import { getAggsMeta } from '../../util/tile_meta_feature_utils'; +import { Mask } from '../vector_layer/mask'; export class HeatmapLayer extends AbstractLayer { private readonly _style: HeatmapStyle; @@ -186,6 +187,19 @@ export class HeatmapLayer extends AbstractLayer { this.syncVisibilityWithMb(mbMap, heatmapLayerId); mbMap.setPaintProperty(heatmapLayerId, 'heatmap-opacity', this.getAlpha()); + + // heatmap can implement mask with filter expression because + // feature-state support is not needed since heatmap layers do not support joins + const maskDescriptor = metricField.getMask(); + if (maskDescriptor) { + const mask = new Mask({ + esAggField: metricField, + isGeometrySourceMvt: true, + ...maskDescriptor, + }); + mbMap.setFilter(heatmapLayerId, mask.getMatchUnmaskedExpression() as FilterSpecification); + } + mbMap.setLayerZoomRange(heatmapLayerId, this.getMinZoom(), this.getMaxZoom()); } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts index ee9fdaf410abb..200c8cad24a4c 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/blended_vector_layer/blended_vector_layer.ts @@ -13,7 +13,6 @@ import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style import { IDynamicStyleProperty } from '../../../styles/vector/properties/dynamic_style_property'; import { IStyleProperty } from '../../../styles/vector/properties/style_property'; import { - COUNT_PROP_LABEL, COUNT_PROP_NAME, GRID_RESOLUTION, LAYER_TYPE, @@ -67,7 +66,6 @@ function getClusterSource(documentSource: IESSource, documentStyle: IVectorStyle clusterSourceDescriptor.metrics = [ { type: AGG_TYPE.COUNT, - label: COUNT_PROP_LABEL, }, ...documentStyle.getDynamicPropertiesArray().map((dynamicProperty) => { return { @@ -267,9 +265,9 @@ export class BlendedVectorLayer extends GeoJsonVectorLayer implements IVectorLay return [clonedDescriptor]; } - getSource(): IVectorSource { + getSource = () => { return this._isClustered ? this._clusterSource : this._documentSource; - } + }; getSourceForEditing() { // Layer is based on this._documentSource diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts new file mode 100644 index 0000000000000..0b1861fd73397 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mask.ts @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { MapGeoJSONFeature } from '@kbn/mapbox-gl'; +import type { IESAggSource } from '../../sources/es_agg_source'; +import type { IESAggField } from '../../fields/agg'; +import { FIELD_ORIGIN, MASK_OPERATOR, MB_LOOKUP_FUNCTION } from '../../../../common/constants'; + +export const BELOW = i18n.translate('xpack.maps.mask.belowLabel', { + defaultMessage: 'below', +}); + +export const ABOVE = i18n.translate('xpack.maps.mask.aboveLabel', { + defaultMessage: 'above', +}); + +export const BUCKETS = i18n.translate('xpack.maps.mask.genericBucketsName', { + defaultMessage: 'buckets', +}); + +const FEATURES = i18n.translate('xpack.maps.mask.genericFeaturesName', { + defaultMessage: 'features', +}); + +const VALUE = i18n.translate('xpack.maps.mask.genericAggLabel', { + defaultMessage: 'value', +}); + +const WHEN = i18n.translate('xpack.maps.mask.when', { + defaultMessage: 'when', +}); + +const WHEN_JOIN_METRIC = i18n.translate('xpack.maps.mask.whenJoinMetric', { + defaultMessage: '{whenLabel} join metric', + values: { + whenLabel: WHEN, + }, +}); + +function getOperatorLabel(operator: MASK_OPERATOR): string { + if (operator === MASK_OPERATOR.BELOW) { + return BELOW; + } + + if (operator === MASK_OPERATOR.ABOVE) { + return ABOVE; + } + + return operator as string; +} + +export function getMaskI18nValue(operator: MASK_OPERATOR, value: number): string { + return `${getOperatorLabel(operator)} ${value}`; +} + +export function getMaskI18nLabel({ + bucketsName, + isJoin, +}: { + bucketsName?: string; + isJoin: boolean; +}): string { + return i18n.translate('xpack.maps.mask.maskLabel', { + defaultMessage: 'Hide {hideNoun}', + values: { + hideNoun: isJoin ? FEATURES : bucketsName ? bucketsName : BUCKETS, + }, + }); +} + +export function getMaskI18nDescription({ + aggLabel, + bucketsName, + isJoin, +}: { + aggLabel?: string; + bucketsName?: string; + isJoin: boolean; +}): string { + return i18n.translate('xpack.maps.mask.maskDescription', { + defaultMessage: '{maskAdverb} {aggLabel} is ', + values: { + aggLabel: aggLabel ? aggLabel : VALUE, + maskAdverb: isJoin ? WHEN_JOIN_METRIC : WHEN, + }, + }); +} + +export class Mask { + private readonly _esAggField: IESAggField; + private readonly _isGeometrySourceMvt: boolean; + private readonly _operator: MASK_OPERATOR; + private readonly _value: number; + + constructor({ + esAggField, + isGeometrySourceMvt, + operator, + value, + }: { + esAggField: IESAggField; + isGeometrySourceMvt: boolean; + operator: MASK_OPERATOR; + value: number; + }) { + this._esAggField = esAggField; + this._isGeometrySourceMvt = isGeometrySourceMvt; + this._operator = operator; + this._value = value; + } + + private _isFeatureState() { + if (this._esAggField.getOrigin() === FIELD_ORIGIN.SOURCE) { + // source fields are stored in properties + return false; + } + + if (!this._isGeometrySourceMvt) { + // For geojson sources, join fields are stored in properties + return false; + } + + // For vector tile sources, it is not possible to add join fields to properties + // so join fields are stored in feature state + return true; + } + + /* + * Returns maplibre expression that matches masked features + */ + getMatchMaskedExpression() { + const comparisionOperator = this._operator === MASK_OPERATOR.BELOW ? '<' : '>'; + const lookup = this._isFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; + return [comparisionOperator, [lookup, this._esAggField.getMbFieldName()], this._value]; + } + + /* + * Returns maplibre expression that matches unmasked features + */ + getMatchUnmaskedExpression() { + const comparisionOperator = this._operator === MASK_OPERATOR.BELOW ? '>=' : '<='; + const lookup = this._isFeatureState() + ? MB_LOOKUP_FUNCTION.FEATURE_STATE + : MB_LOOKUP_FUNCTION.GET; + return [comparisionOperator, [lookup, this._esAggField.getMbFieldName()], this._value]; + } + + getEsAggField() { + return this._esAggField; + } + + getFieldOriginListLabel() { + const source = this._esAggField.getSource(); + const isJoin = this._esAggField.getOrigin() === FIELD_ORIGIN.JOIN; + const maskLabel = getMaskI18nLabel({ + bucketsName: + 'getBucketsName' in (source as IESAggSource) + ? (source as IESAggSource).getBucketsName() + : undefined, + isJoin, + }); + const adverb = isJoin ? WHEN_JOIN_METRIC : WHEN; + + return `${maskLabel} ${adverb}`; + } + + getOperator() { + return this._operator; + } + + getValue() { + return this._value; + } + + isFeatureMasked(feature: MapGeoJSONFeature) { + const featureValue = this._isFeatureState() + ? feature?.state[this._esAggField.getMbFieldName()] + : feature?.properties[this._esAggField.getMbFieldName()]; + if (typeof featureValue !== 'number') { + return false; + } + return this._operator === MASK_OPERATOR.BELOW + ? featureValue < this._value + : featureValue > this._value; + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx index a976837ee2881..82bb15c19ffca 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/mvt_vector_layer/mvt_vector_layer.test.tsx @@ -25,6 +25,7 @@ import { } from '../../../../../common/descriptor_types'; import { LAYER_TYPE, SOURCE_TYPES } from '../../../../../common/constants'; import { MvtVectorLayer } from './mvt_vector_layer'; +import { ITermJoinSource } from '../../../sources/term_join_source'; const defaultConfig = { urlTemplate: 'https://example.com/{x}/{y}/{z}.pbf', @@ -176,6 +177,9 @@ describe('isLayerLoading', () => { getSourceDataRequestId: () => { return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2'; }, + getRightJoinSource: () => { + return {} as unknown as ITermJoinSource; + }, } as unknown as InnerJoin, ], layerDescriptor: { @@ -212,6 +216,9 @@ describe('isLayerLoading', () => { getSourceDataRequestId: () => { return 'join_source_a0b0da65-5e1a-4967-9dbe-74f24391afe2'; }, + getRightJoinSource: () => { + return {} as unknown as ITermJoinSource; + }, } as unknown as InnerJoin, ], layerDescriptor: { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index dbee11b617dec..366c9cde6eee6 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -58,12 +58,14 @@ import { ITooltipProperty } from '../../tooltips/tooltip_property'; import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; import { IESSource } from '../../sources/es_source'; import { ITermJoinSource } from '../../sources/term_join_source'; +import type { IESAggSource } from '../../sources/es_agg_source'; import { buildVectorRequestMeta } from '../build_vector_request_meta'; import { getJoinAggKey } from '../../../../common/get_agg_key'; import { syncBoundsData } from './bounds_data'; import { JoinState } from './types'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { PropertiesMap } from '../../../../common/elasticsearch_util'; +import { Mask } from './mask'; const SUPPORTS_FEATURE_EDITING_REQUEST_ID = 'SUPPORTS_FEATURE_EDITING_REQUEST_ID'; @@ -106,6 +108,7 @@ export interface IVectorLayer extends ILayer { getLeftJoinFields(): Promise; addFeature(geometry: Geometry | Position[]): Promise; deleteFeature(featureId: string): Promise; + getMasks(): Mask[]; } export const noResultsIcon = ; @@ -120,6 +123,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { protected readonly _style: VectorStyle; private readonly _joins: InnerJoin[]; protected readonly _descriptor: VectorLayerDescriptor; + private readonly _masks: Mask[]; static createDescriptor( options: Partial, @@ -163,6 +167,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { customIcons, chartsPaletteServiceGetColor ); + this._masks = this._createMasks(); } async cloneDescriptor(): Promise { @@ -692,6 +697,69 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { return undefined; } + _createMasks() { + const masks: Mask[] = []; + const source = this.getSource(); + if ('getMetricFields' in (source as IESAggSource)) { + const metricFields = (source as IESAggSource).getMetricFields(); + metricFields.forEach((metricField) => { + const maskDescriptor = metricField.getMask(); + if (maskDescriptor) { + masks.push( + new Mask({ + esAggField: metricField, + isGeometrySourceMvt: source.isMvt(), + ...maskDescriptor, + }) + ); + } + }); + } + + this.getValidJoins().forEach((join) => { + const rightSource = join.getRightJoinSource(); + if ('getMetricFields' in (rightSource as unknown as IESAggSource)) { + const metricFields = (rightSource as unknown as IESAggSource).getMetricFields(); + metricFields.forEach((metricField) => { + const maskDescriptor = metricField.getMask(); + if (maskDescriptor) { + masks.push( + new Mask({ + esAggField: metricField, + isGeometrySourceMvt: source.isMvt(), + ...maskDescriptor, + }) + ); + } + }); + } + }); + + return masks; + } + + getMasks() { + return this._masks; + } + + // feature-state is not supported in filter expressions + // https://github.com/mapbox/mapbox-gl-js/issues/8487 + // therefore, masking must be accomplished via setting opacity paint property (hack) + _getAlphaExpression() { + const maskCaseExpressions: unknown[] = []; + this.getMasks().forEach((mask) => { + // case expressions require 2 parts + // 1) condition expression + maskCaseExpressions.push(mask.getMatchMaskedExpression()); + // 2) output. 0 opacity styling "hides" feature + maskCaseExpressions.push(0); + }); + + return maskCaseExpressions.length + ? ['case', ...maskCaseExpressions, this.getAlpha()] + : this.getAlpha(); + } + _setMbPointsProperties( mbMap: MbMap, mvtSourceLayer?: string, @@ -759,13 +827,13 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { if (this.getCurrentStyle().arePointsSymbolizedAsCircles()) { this.getCurrentStyle().setMBPaintPropertiesForPoints({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, pointLayerId: markerLayerId, }); } else { this.getCurrentStyle().setMBSymbolPropertiesForPoints({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, symbolLayerId: markerLayerId, }); @@ -811,7 +879,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { } this.getCurrentStyle().setMBPaintProperties({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, fillLayerId, lineLayerId, @@ -865,7 +933,7 @@ export class AbstractVectorLayer extends AbstractLayer implements IVectorLayer { } this.getCurrentStyle().setMBPropertiesForLabelText({ - alpha: this.getAlpha(), + alpha: this._getAlphaExpression(), mbMap, textLayerId: labelLayerId, }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts index fa7f329beb97a..dda3026ddd4ef 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.ts @@ -11,11 +11,13 @@ import { DataView } from '@kbn/data-plugin/common'; import type { IESAggSource } from './types'; import { AbstractESSource } from '../es_source'; import { esAggFieldsFactory, IESAggField } from '../../fields/agg'; -import { AGG_TYPE, COUNT_PROP_LABEL, FIELD_ORIGIN } from '../../../../common/constants'; +import { AGG_TYPE, FIELD_ORIGIN } from '../../../../common/constants'; import { getSourceAggKey } from '../../../../common/get_agg_key'; import { AbstractESAggSourceDescriptor, AggDescriptor } from '../../../../common/descriptor_types'; import { IField } from '../../fields/field'; import { ITooltipProperty } from '../../tooltips/tooltip_property'; +import { getAggDisplayName } from './get_agg_display_name'; +import { BUCKETS } from '../../layers/vector_layer/mask'; export const DEFAULT_METRIC = { type: AGG_TYPE.COUNT }; @@ -46,6 +48,10 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE } } + getBucketsName() { + return BUCKETS; + } + getFieldByName(fieldName: string): IField | null { return this.getMetricFieldForName(fieldName); } @@ -83,14 +89,14 @@ export abstract class AbstractESAggSource extends AbstractESSource implements IE async getAggLabel(aggType: AGG_TYPE, fieldLabel: string): Promise { switch (aggType) { case AGG_TYPE.COUNT: - return COUNT_PROP_LABEL; + return getAggDisplayName(aggType); case AGG_TYPE.TERMS: return i18n.translate('xpack.maps.source.esAggSource.topTermLabel', { - defaultMessage: `Top {fieldLabel}`, + defaultMessage: `top {fieldLabel}`, values: { fieldLabel }, }); default: - return `${aggType} ${fieldLabel}`; + return `${getAggDisplayName(aggType)} ${fieldLabel}`; } } diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/get_agg_display_name.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/get_agg_display_name.ts new file mode 100644 index 0000000000000..516f6448fb629 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/get_agg_display_name.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { AGG_TYPE } from '../../../../common/constants'; + +export function getAggDisplayName(aggType: AGG_TYPE): string { + switch (aggType) { + case AGG_TYPE.AVG: + return i18n.translate('xpack.maps.aggType.averageLabel', { + defaultMessage: 'average', + }); + case AGG_TYPE.COUNT: + return i18n.translate('xpack.maps.aggType.countLabel', { + defaultMessage: 'count', + }); + case AGG_TYPE.MAX: + return i18n.translate('xpack.maps.aggType.maximumLabel', { + defaultMessage: 'max', + }); + case AGG_TYPE.MIN: + return i18n.translate('xpack.maps.aggType.minimumLabel', { + defaultMessage: 'min', + }); + case AGG_TYPE.PERCENTILE: + return i18n.translate('xpack.maps.aggType.percentileLabel', { + defaultMessage: 'percentile', + }); + case AGG_TYPE.SUM: + return i18n.translate('xpack.maps.aggType.sumLabel', { + defaultMessage: 'sum', + }); + case AGG_TYPE.TERMS: + return i18n.translate('xpack.maps.aggType.topTermLabel', { + defaultMessage: 'top term', + }); + case AGG_TYPE.UNIQUE_COUNT: + return i18n.translate('xpack.maps.aggType.cardinalityTermLabel', { + defaultMessage: 'unique count', + }); + default: + return aggType; + } +} diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts index 033d937aa9391..80bc74bd4ceb4 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts @@ -7,3 +7,4 @@ export type { IESAggSource } from './types'; export { AbstractESAggSource, DEFAULT_METRIC } from './es_agg_source'; +export { getAggDisplayName } from './get_agg_display_name'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts index d9cb6fcd95a10..697ae8a1a606d 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_agg_source/types.ts @@ -13,6 +13,12 @@ import { IESAggField } from '../../fields/agg'; export interface IESAggSource extends IESSource { getAggKey(aggType: AGG_TYPE, fieldName: string): string; getAggLabel(aggType: AGG_TYPE, fieldLabel: string): Promise; + + /* + * Returns human readable name describing buckets, like "clusters" or "grids" + */ + getBucketsName(): string; + getMetricFields(): IESAggField[]; getMetricFieldForName(fieldName: string): IESAggField | null; getValueAggsDsl(indexPattern: DataView): { [key: string]: unknown }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap index 79b8c4ffc9808..fe50d54ca5535 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/__snapshots__/update_source_editor.test.tsx.snap @@ -19,7 +19,9 @@ exports[`source editor geo_grid_source should not allow editing multiple metrics /> { async function onChange(...sourceChanges: OnSourceChangeArgs[]) { sourceEditorArgs.onChange(...sourceChanges); @@ -129,6 +147,7 @@ export class ESGeoGridSource extends AbstractESAggSource implements IMvtVectorSo } return ( ({ })); const defaultProps = { + bucketsName: 'clusters', currentLayerType: LAYER_TYPE.GEOJSON_VECTOR, geoFieldName: 'myLocation', indexPatternId: 'foobar', diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx index 3268992c7f2b6..d69a97d09dd47 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.tsx @@ -6,7 +6,6 @@ */ import React, { Fragment, Component } from 'react'; - import { v4 as uuidv4 } from 'uuid'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiPanel, EuiSpacer, EuiComboBoxOptionOption, EuiTitle } from '@elastic/eui'; @@ -25,6 +24,7 @@ import { clustersTitle, heatmapTitle } from './es_geo_grid_source'; import { isMvt } from './is_mvt'; interface Props { + bucketsName: string; currentLayerType?: string; geoFieldName: string; indexPatternId: string; @@ -148,6 +148,8 @@ export class UpdateSourceEditor extends Component { { ); } diff --git a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.tsx similarity index 74% rename from x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.tsx index 856504c51865e..c36cc3d49089a 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.tsx @@ -6,17 +6,31 @@ */ import React, { Component, Fragment } from 'react'; - -import { getDataViewNotFoundMessage } from '../../../../common/i18n_getters'; -import { MetricsEditor } from '../../../components/metrics_editor'; -import { getIndexPatternService } from '../../../kibana_services'; +import type { DataViewField } from '@kbn/data-plugin/common'; import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { indexPatterns } from '@kbn/data-plugin/public'; +import { MetricsEditor } from '../../../components/metrics_editor'; +import { getIndexPatternService } from '../../../kibana_services'; +import type { AggDescriptor } from '../../../../common/descriptor_types'; +import type { OnSourceChangeArgs } from '../source'; + +interface Props { + bucketsName: string; + indexPatternId: string; + metrics: AggDescriptor[]; + onChange: (...args: OnSourceChangeArgs[]) => void; +} + +interface State { + fields: DataViewField[]; +} + +export class UpdateSourceEditor extends Component { + private _isMounted: boolean = false; -export class UpdateSourceEditor extends Component { state = { - fields: null, + fields: [], }; componentDidMount() { @@ -33,11 +47,6 @@ export class UpdateSourceEditor extends Component { try { indexPattern = await getIndexPatternService().get(this.props.indexPatternId); } catch (err) { - if (this._isMounted) { - this.setState({ - loadError: getDataViewNotFoundMessage(this.props.indexPatternId), - }); - } return; } @@ -50,7 +59,7 @@ export class UpdateSourceEditor extends Component { }); } - _onMetricsChange = (metrics) => { + _onMetricsChange = (metrics: AggDescriptor[]) => { this.props.onChange({ propName: 'metrics', value: metrics }); }; @@ -69,6 +78,8 @@ export class UpdateSourceEditor extends Component { { }); const metrics = source.getMetricFields(); expect(metrics[0].getName()).toEqual('__kbnjoin__count__1234'); - expect(await metrics[0].getLabel()).toEqual('Count of foobar'); + expect(await metrics[0].getLabel()).toEqual('count of foobar'); }); it('should override name and label of sum metric', async () => { @@ -51,7 +51,7 @@ describe('getMetricFields', () => { expect(metrics[0].getName()).toEqual('__kbnjoin__sum_of_myFieldGettingSummed__1234'); expect(await metrics[0].getLabel()).toEqual('my custom label'); expect(metrics[1].getName()).toEqual('__kbnjoin__count__1234'); - expect(await metrics[1].getLabel()).toEqual('Count of foobar'); + expect(await metrics[1].getLabel()).toEqual('count of foobar'); }); }); diff --git a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts index 5540454702114..4c7793e1b01cb 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.ts @@ -31,6 +31,7 @@ import { import { PropertiesMap } from '../../../../common/elasticsearch_util'; import { isValidStringConfig } from '../../util/valid_string_config'; import { ITermJoinSource } from '../term_join_source'; +import type { IESAggSource } from '../es_agg_source'; import { IField } from '../../fields/field'; import { mergeExecutionContext } from '../execution_context_utils'; @@ -52,7 +53,7 @@ export function extractPropertiesMap(rawEsData: any, countPropertyName: string): return propertiesMap; } -export class ESTermSource extends AbstractESAggSource implements ITermJoinSource { +export class ESTermSource extends AbstractESAggSource implements ITermJoinSource, IESAggSource { static type = SOURCE_TYPES.ES_TERM_SOURCE; static createDescriptor(descriptor: Partial): ESTermSourceDescriptor { @@ -115,7 +116,7 @@ export class ESTermSource extends AbstractESAggSource implements ITermJoinSource } return aggType === AGG_TYPE.COUNT ? i18n.translate('xpack.maps.source.esJoin.countLabel', { - defaultMessage: `Count of {indexPatternLabel}`, + defaultMessage: `count of {indexPatternLabel}`, values: { indexPatternLabel }, }) : super.getAggLabel(aggType, fieldLabel); diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx index 390e48408d747..027e4dc29c58c 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.tsx @@ -5,13 +5,15 @@ * 2.0. */ -import React, { Component } from 'react'; +import React, { Component, ReactNode } from 'react'; import { i18n } from '@kbn/i18n'; import { ColorGradient } from './color_gradient'; import { RangedStyleLegendRow } from '../../../components/ranged_style_legend_row'; import { HEATMAP_COLOR_RAMP_LABEL } from '../heatmap_constants'; -import { IField } from '../../../../fields/field'; +import type { IField } from '../../../../fields/field'; +import type { IESAggField } from '../../../../fields/agg'; +import { MaskLegend } from '../../../vector/components/legend/mask_legend'; interface Props { colorRampName: string; @@ -47,7 +49,7 @@ export class HeatmapLegend extends Component { } render() { - return ( + const metricLegend = ( } minLabel={i18n.translate('xpack.maps.heatmapLegend.coldLabel', { @@ -61,5 +63,28 @@ export class HeatmapLegend extends Component { invert={false} /> ); + + let maskLegend: ReactNode | undefined; + if ('getMask' in (this.props.field as IESAggField)) { + const mask = (this.props.field as IESAggField).getMask(); + if (mask) { + maskLegend = ( + + ); + } + } + + return maskLegend ? ( + <> + {maskLegend} + {metricLegend} + + ) : ( + metricLegend + ); } } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/mask_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/mask_legend.tsx new file mode 100644 index 0000000000000..4ffb7ba5a1834 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/mask_legend.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { FIELD_ORIGIN, MASK_OPERATOR } from '../../../../../../common/constants'; +import type { IESAggField } from '../../../../fields/agg'; +import type { IESAggSource } from '../../../../sources/es_agg_source'; +import { + getMaskI18nDescription, + getMaskI18nLabel, + getMaskI18nValue, +} from '../../../../layers/vector_layer/mask'; + +interface Props { + esAggField: IESAggField; + onlyShowLabelAndValue?: boolean; + operator: MASK_OPERATOR; + value: number; +} + +interface State { + aggLabel?: string; +} + +export class MaskLegend extends Component { + private _isMounted = false; + + state: State = {}; + + componentDidMount() { + this._isMounted = true; + this._loadAggLabel(); + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidUpdate() { + this._loadAggLabel(); + } + + _loadAggLabel = async () => { + const aggLabel = await this.props.esAggField.getLabel(); + if (this._isMounted && aggLabel !== this.state.aggLabel) { + this.setState({ aggLabel }); + } + }; + + _getBucketsName() { + const source = this.props.esAggField.getSource(); + return 'getBucketsName' in (source as IESAggSource) + ? (source as IESAggSource).getBucketsName() + : undefined; + } + + _getPrefix() { + if (this.props.onlyShowLabelAndValue) { + return i18n.translate('xpack.maps.maskLegend.is', { + defaultMessage: '{aggLabel} is', + values: { + aggLabel: this.state.aggLabel, + }, + }); + } + + const isJoin = this.props.esAggField.getOrigin() === FIELD_ORIGIN.JOIN; + const maskLabel = getMaskI18nLabel({ + bucketsName: this._getBucketsName(), + isJoin, + }); + const maskDescription = getMaskI18nDescription({ + aggLabel: this.state.aggLabel, + isJoin, + }); + return `${maskLabel} ${maskDescription}`; + } + + render() { + return ( + + + {`${this._getPrefix()} `} + {getMaskI18nValue(this.props.operator, this.props.value)} + + + ); + } +} diff --git a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx index 2d282a4b530cb..60bcd05c9f738 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.tsx @@ -6,17 +6,30 @@ */ import React from 'react'; +import { EuiText } from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { FIELD_ORIGIN } from '../../../../../../common/constants'; +import { Mask } from '../../../../layers/vector_layer/mask'; import { IStyleProperty } from '../../properties/style_property'; +import { MaskLegend } from './mask_legend'; interface Props { isLinesOnly: boolean; isPointsOnly: boolean; + masks: Mask[]; styles: Array>; symbolId?: string; svg?: string; } -export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId, svg }: Props) { +export function VectorStyleLegend({ + isLinesOnly, + isPointsOnly, + masks, + styles, + symbolId, + svg, +}: Props) { const legendRows = []; for (let i = 0; i < styles.length; i++) { @@ -34,5 +47,55 @@ export function VectorStyleLegend({ isLinesOnly, isPointsOnly, styles, symbolId, ); } - return <>{legendRows}; + function renderMasksByFieldOrigin(fieldOrigin: FIELD_ORIGIN) { + const masksByFieldOrigin = masks.filter( + (mask) => mask.getEsAggField().getOrigin() === fieldOrigin + ); + if (masksByFieldOrigin.length === 0) { + return null; + } + + if (masksByFieldOrigin.length === 1) { + const mask = masksByFieldOrigin[0]; + return ( + + ); + } + + return ( + <> + + {masksByFieldOrigin[0].getFieldOriginListLabel()} + +
    + {masksByFieldOrigin.map((mask) => ( +
  • + +
  • + ))} +
+ + ); + } + + return ( + <> + {renderMasksByFieldOrigin(FIELD_ORIGIN.SOURCE)} + {renderMasksByFieldOrigin(FIELD_ORIGIN.JOIN)} + {legendRows} + + ); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx index 6daa8cf84afaa..f1a55b571e27d 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx @@ -51,7 +51,7 @@ export class DynamicColorProperty extends DynamicStyleProperty { - syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'circle-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'circle-opacity', alpha); } - syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncFillColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'fill-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'fill-opacity', alpha); } @@ -28,17 +28,17 @@ export class StaticColorProperty extends StaticStyleProperty mbMap.setPaintProperty(mbLayerId, 'icon-halo-color', this._options.color); } - syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncLineColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'line-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'line-opacity', alpha); } - syncCircleStrokeWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncCircleStrokeWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'circle-stroke-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'circle-stroke-opacity', alpha); } - syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { + syncLabelColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: unknown) { mbMap.setPaintProperty(mbLayerId, 'text-color', this._options.color); mbMap.setPaintProperty(mbLayerId, 'text-opacity', alpha); } diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index 3b64d0960628c..31c06728d8112 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -130,7 +130,7 @@ export interface IVectorStyle extends IStyle { fillLayerId, lineLayerId, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; fillLayerId: string; lineLayerId: string; @@ -140,7 +140,7 @@ export interface IVectorStyle extends IStyle { mbMap, pointLayerId, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; pointLayerId: string; }) => void; @@ -149,7 +149,7 @@ export interface IVectorStyle extends IStyle { mbMap, textLayerId, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; textLayerId: string; }) => void; @@ -158,7 +158,7 @@ export interface IVectorStyle extends IStyle { symbolLayerId, alpha, }: { - alpha: number; + alpha: unknown; mbMap: MbMap; symbolLayerId: string; }) => void; @@ -730,6 +730,7 @@ export class VectorStyle implements IVectorStyle { return ( void; + onClose: () => void; +} + +interface State { + operator: MASK_OPERATOR; + value: number | string; +} + +export class MaskEditor extends Component { + constructor(props: Props) { + super(props); + this.state = { + operator: + this.props.metric.mask !== undefined + ? this.props.metric.mask.operator + : MASK_OPERATOR.BELOW, + value: this.props.metric.mask !== undefined ? this.props.metric.mask.value : '', + }; + } + + _onSet = () => { + if (this._isValueInValid()) { + return; + } + + this.props.onChange({ + ...this.props.metric, + mask: { + operator: this.state.operator, + value: this.state.value as number, + }, + }); + this.props.onClose(); + }; + + _onClear = () => { + const newMetric = { + ...this.props.metric, + }; + delete newMetric.mask; + this.props.onChange(newMetric); + this.props.onClose(); + }; + + _onOperatorChange = (e: ChangeEvent) => { + this.setState({ + operator: e.target.value as MASK_OPERATOR, + }); + }; + + _onValueChange = (evt: ChangeEvent) => { + const sanitizedValue = parseFloat(evt.target.value); + this.setState({ + value: isNaN(sanitizedValue) ? evt.target.value : sanitizedValue, + }); + }; + + _hasChanges() { + return ( + this.props.metric.mask === undefined || + this.props.metric.mask.operator !== this.state.operator || + this.props.metric.mask.value !== this.state.value + ); + } + + _isValueInValid() { + return typeof this.state.value === 'string'; + } + + _renderForm() { + return ( + + + + + + + + + + + + + ); + } + + _renderFooter() { + return ( + + + + + {panelStrings.close} + + + + + {panelStrings.clear} + + + + + {panelStrings.apply} + + + + + ); + } + + render() { + return ( + <> + {this._renderForm()} + + {this._renderFooter()} + + ); + } +} diff --git a/x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_expression.tsx b/x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_expression.tsx new file mode 100644 index 0000000000000..e05fdc6cdf2d5 --- /dev/null +++ b/x-pack/plugins/maps/public/components/metrics_editor/mask_expression/mask_expression.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component } from 'react'; +import { EuiExpression, EuiPopover } from '@elastic/eui'; +import { DataViewField } from '@kbn/data-views-plugin/public'; +import { AGG_TYPE } from '../../../../common/constants'; +import { AggDescriptor, FieldedAggDescriptor } from '../../../../common/descriptor_types'; +import { MaskEditor } from './mask_editor'; +import { getAggDisplayName } from '../../../classes/sources/es_agg_source'; +import { + getMaskI18nDescription, + getMaskI18nValue, +} from '../../../classes/layers/vector_layer/mask'; + +interface Props { + fields: DataViewField[]; + isJoin: boolean; + metric: AggDescriptor; + onChange: (metric: AggDescriptor) => void; +} + +interface State { + isPopoverOpen: boolean; +} + +export class MaskExpression extends Component { + state: State = { + isPopoverOpen: false, + }; + + _togglePopover = () => { + this.setState((prevState) => ({ + isPopoverOpen: !prevState.isPopoverOpen, + })); + }; + + _closePopover = () => { + this.setState({ + isPopoverOpen: false, + }); + }; + + _getMaskExpressionValue() { + return this.props.metric.mask === undefined + ? '...' + : getMaskI18nValue(this.props.metric.mask.operator, this.props.metric.mask.value); + } + + _getAggLabel() { + const aggDisplayName = getAggDisplayName(this.props.metric.type); + if (this.props.metric.type === AGG_TYPE.COUNT || this.props.metric.field === undefined) { + return aggDisplayName; + } + + const targetField = this.props.fields.find( + (field) => field.name === (this.props.metric as FieldedAggDescriptor).field + ); + const fieldDisplayName = targetField?.displayName + ? targetField?.displayName + : this.props.metric.field; + return `${aggDisplayName} ${fieldDisplayName}`; + } + + render() { + // masks only supported for numerical metrics + if (this.props.metric.type === AGG_TYPE.TERMS) { + return null; + } + + return ( + + } + isOpen={this.state.isPopoverOpen} + closePopover={this._closePopover} + panelPaddingSize="s" + anchorPosition="downCenter" + repositionOnScroll={true} + > + + + ); + } +} diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx index 26cf6d5313821..5042cb4ffa9c7 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx @@ -18,6 +18,8 @@ import { AggDescriptor } from '../../../common/descriptor_types'; import { AGG_TYPE, DEFAULT_PERCENTILE } from '../../../common/constants'; import { getTermsFields } from '../../index_pattern_util'; import { ValidatedNumberInput } from '../validated_number_input'; +import { getMaskI18nLabel } from '../../classes/layers/vector_layer/mask'; +import { MaskExpression } from './mask_expression'; function filterFieldsForAgg(fields: DataViewField[], aggType: AGG_TYPE) { if (!fields) { @@ -43,6 +45,8 @@ function filterFieldsForAgg(fields: DataViewField[], aggType: AGG_TYPE) { } interface Props { + bucketsName?: string; + isJoin: boolean; metric: AggDescriptor; fields: DataViewField[]; onChange: (metric: AggDescriptor) => void; @@ -52,7 +56,9 @@ interface Props { } export function MetricEditor({ + bucketsName, fields, + isJoin, metricsFilter, metric, onChange, @@ -64,6 +70,8 @@ export function MetricEditor({ return; } + // Intentionally not adding mask. + // Changing aggregation likely changes value range so keeping old mask does not seem relevent const descriptor = { type: metricAggregationType, label: metric.label, @@ -93,6 +101,8 @@ export function MetricEditor({ if (!fieldName || metric.type === AGG_TYPE.COUNT) { return; } + // Intentionally not adding mask. + // Changing field likely changes value range so keeping old mask does not seem relevent onChange({ label: metric.label, type: metric.type, @@ -223,6 +233,11 @@ export function MetricEditor({ {fieldSelect} {percentileSelect} {labelInput} + + + + + {removeButton} ); diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx index e8dd63fe934e5..88c9b83bc54a4 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx @@ -9,54 +9,39 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; import { AGG_TYPE } from '../../../common/constants'; +import { getAggDisplayName } from '../../classes/sources/es_agg_source'; const AGG_OPTIONS = [ { - label: i18n.translate('xpack.maps.metricSelect.averageDropDownOptionLabel', { - defaultMessage: 'Average', - }), + label: getAggDisplayName(AGG_TYPE.AVG), value: AGG_TYPE.AVG, }, { - label: i18n.translate('xpack.maps.metricSelect.countDropDownOptionLabel', { - defaultMessage: 'Count', - }), + label: getAggDisplayName(AGG_TYPE.COUNT), value: AGG_TYPE.COUNT, }, { - label: i18n.translate('xpack.maps.metricSelect.maxDropDownOptionLabel', { - defaultMessage: 'Max', - }), + label: getAggDisplayName(AGG_TYPE.MAX), value: AGG_TYPE.MAX, }, { - label: i18n.translate('xpack.maps.metricSelect.minDropDownOptionLabel', { - defaultMessage: 'Min', - }), + label: getAggDisplayName(AGG_TYPE.MIN), value: AGG_TYPE.MIN, }, { - label: i18n.translate('xpack.maps.metricSelect.percentileDropDownOptionLabel', { - defaultMessage: 'Percentile', - }), + label: getAggDisplayName(AGG_TYPE.PERCENTILE), value: AGG_TYPE.PERCENTILE, }, { - label: i18n.translate('xpack.maps.metricSelect.sumDropDownOptionLabel', { - defaultMessage: 'Sum', - }), + label: getAggDisplayName(AGG_TYPE.SUM), value: AGG_TYPE.SUM, }, { - label: i18n.translate('xpack.maps.metricSelect.termsDropDownOptionLabel', { - defaultMessage: 'Top term', - }), + label: getAggDisplayName(AGG_TYPE.TERMS), value: AGG_TYPE.TERMS, }, { - label: i18n.translate('xpack.maps.metricSelect.cardinalityDropDownOptionLabel', { - defaultMessage: 'Unique count', - }), + label: getAggDisplayName(AGG_TYPE.UNIQUE_COUNT), value: AGG_TYPE.UNIQUE_COUNT, }, ]; diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx index 66fed40936b79..5aaf1369efe81 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx @@ -20,6 +20,7 @@ const defaultProps = { fields: [], onChange: () => {}, allowMultipleMetrics: true, + isJoin: false, }; test('should render metrics editor', () => { diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx index b38e20b40d990..a18608b9631c2 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx @@ -22,6 +22,8 @@ export function isMetricValid(aggDescriptor: AggDescriptor) { interface Props { allowMultipleMetrics: boolean; + bucketsName?: string; + isJoin: boolean; metrics: AggDescriptor[]; fields: DataViewField[]; onChange: (metrics: AggDescriptor[]) => void; @@ -81,6 +83,8 @@ export class MetricsEditor extends Component { return (
{ metrics={this.props.metrics} onChange={this.props.onChange} allowMultipleMetrics={true} + isJoin={true} /> ); }; diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap index cb496311b3d1c..46ddc7f58eb78 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap @@ -76,11 +76,7 @@ exports[`Should render edit form row when attribution not provided 1`] = ` onClick={[Function]} size="xs" > - + Clear
diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx index bdab63e1029e7..38ac195904592 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { EuiButtonEmpty, EuiLink, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; import { Attribution } from '../../../../common/descriptor_types'; import { ILayer } from '../../../classes/layers/layer'; import { AttributionPopover } from './attribution_popover'; +import { panelStrings } from '../../panel_strings'; interface Props { layer: ILayer; @@ -65,9 +65,7 @@ export function AttributionFormRow(props: Props) { defaultMessage: 'Edit attribution', } )} - popoverButtonLabel={i18n.translate('xpack.maps.attribution.editBtnLabel', { - defaultMessage: 'Edit', - })} + popoverButtonLabel={panelStrings.edit} label={layerDescriptor.attribution.label} url={layerDescriptor.attribution.url} /> @@ -83,10 +81,7 @@ export function AttributionFormRow(props: Props) { defaultMessage: 'Clear attribution', })} > - + {panelStrings.clear}
diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx index 0371b68c85a3b..530b3cce20f00 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx @@ -20,6 +20,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { Attribution } from '../../../../common/descriptor_types'; +import { panelStrings } from '../../panel_strings'; interface Props { onChange: (attribution: Attribution) => void; @@ -128,7 +129,7 @@ export class AttributionPopover extends Component { onClick={this._onApply} size="s" > - + {panelStrings.apply} diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx index 8d399f19a765c..02b9048e93b86 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx @@ -35,7 +35,7 @@ export function StyleSettings({ layer, updateStyleDescriptor, updateCustomIcons
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index da4765ca094ec..71858ecb02459 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -283,11 +283,10 @@ export class MbMap extends Component { } _initResizerChecker() { + this.state.mbMap?.resize(); // ensure map is sized for container prior to monitoring this._checker = new ResizeChecker(this._containerRef!); this._checker.on('resize', () => { - if (this.state.mbMap) { - this.state.mbMap.resize(); - } + this.state.mbMap?.resize(); }); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx index edd27d2d1edb1..9074fbd8fbeaf 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.test.tsx @@ -70,6 +70,9 @@ const mockLayer = { }, } as unknown as IVectorSource; }, + getMasks: () => { + return []; + }, } as unknown as IVectorLayer; const mockMbMapHandlers: { [key: string]: (event?: MapMouseEvent) => void } = {}; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx index 1c14d11eb1f96..75e41464fd0f8 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_control.tsx @@ -184,6 +184,15 @@ export class TooltipControl extends Component { continue; } + // masking must use paint property "opacity" to hide features in order to support feature state + // therefore, there is no way to remove masked features with queryRenderedFeatures + // masked features must be removed via manual filtering + const masks = layer.getMasks(); + const maskHiddingFeature = masks.find((mask) => mask.isFeatureMasked(mbFeature)); + if (maskHiddingFeature) { + continue; + } + const featureId = layer.getFeatureId(mbFeature); if (featureId === undefined) { continue; diff --git a/x-pack/plugins/maps/public/connected_components/panel_strings.ts b/x-pack/plugins/maps/public/connected_components/panel_strings.ts index f7f7278138e1e..f4eb5e871a3fe 100644 --- a/x-pack/plugins/maps/public/connected_components/panel_strings.ts +++ b/x-pack/plugins/maps/public/connected_components/panel_strings.ts @@ -8,12 +8,21 @@ import { i18n } from '@kbn/i18n'; export const panelStrings = { + apply: i18n.translate('xpack.maps.panel.applyLabel', { + defaultMessage: 'Apply', + }), + clear: i18n.translate('xpack.maps.panel.clearLabel', { + defaultMessage: 'Clear', + }), close: i18n.translate('xpack.maps.panel.closeLabel', { defaultMessage: 'Close', }), discardChanges: i18n.translate('xpack.maps.panel.discardChangesLabel', { defaultMessage: 'Discard changes', }), + edit: i18n.translate('xpack.maps.panel.editLabel', { + defaultMessage: 'Edit', + }), keepChanges: i18n.translate('xpack.maps.panel.keepChangesLabel', { defaultMessage: 'Keep changes', }), diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index 01eba4ee7905e..c4bddf5a71541 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -86,8 +86,7 @@ export async function renderApp( mapEmbeddableInput = { savedObjectId: routeProps.match.params.savedMapId, } as MapByReferenceInput; - } - if (valueInput) { + } else if (valueInput) { mapEmbeddableInput = valueInput as MapByValueInput; } diff --git a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx index 26cb872006dee..f988c5b11ef98 100644 --- a/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/load_list_and_render.tsx @@ -5,12 +5,10 @@ * 2.0. */ -import React, { Component } from 'react'; -import { i18n } from '@kbn/i18n'; +import React, { useState, useEffect } from 'react'; import { Redirect } from 'react-router-dom'; import { EmbeddableStateTransfer } from '@kbn/embeddable-plugin/public'; import { ScopedHistory } from '@kbn/core/public'; -import { getToasts } from '../../kibana_services'; import { MapsListView } from './maps_list_view'; import { APP_ID } from '../../../common/constants'; import { mapsClient } from '../../content_management'; @@ -20,49 +18,39 @@ interface Props { stateTransfer: EmbeddableStateTransfer; } -export class LoadListAndRender extends Component { - _isMounted: boolean = false; - state = { - mapsLoaded: false, - hasSavedMaps: null, - }; - - componentDidMount() { - this._isMounted = true; - this.props.stateTransfer.clearEditorState(APP_ID); - this._loadMapsList(); - } - - componentWillUnmount() { - this._isMounted = false; +export function LoadListAndRender(props: Props) { + const [mapsLoaded, setMapsLoaded] = useState(false); + const [hasSavedMaps, setHasSavedMaps] = useState(true); + + useEffect(() => { + props.stateTransfer.clearEditorState(APP_ID); + + let ignore = false; + mapsClient + .search({ limit: 1 }) + .then((results) => { + if (!ignore) { + setHasSavedMaps(results.hits.length > 0); + setMapsLoaded(true); + } + }) + .catch((err) => { + if (!ignore) { + setMapsLoaded(true); + setHasSavedMaps(false); + } + }); + return () => { + ignore = true; + }; + // only run on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!mapsLoaded) { + // do not render loading state to avoid UI flash when listing page is displayed + return null; } - async _loadMapsList() { - try { - const results = await mapsClient.search({ limit: 1 }); - if (this._isMounted) { - this.setState({ mapsLoaded: true, hasSavedMaps: !!results.hits.length }); - } - } catch (err) { - if (this._isMounted) { - this.setState({ mapsLoaded: true, hasSavedMaps: false }); - getToasts().addDanger({ - title: i18n.translate('xpack.maps.mapListing.errorAttemptingToLoadSavedMaps', { - defaultMessage: `Unable to load maps`, - }), - text: `${err}`, - }); - } - } - } - - render() { - const { mapsLoaded, hasSavedMaps } = this.state; - - if (mapsLoaded) { - return hasSavedMaps ? : ; - } else { - return null; - } - } + return hasSavedMaps ? : ; } diff --git a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx index 95063f728a8fc..d98444057097d 100644 --- a/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx +++ b/x-pack/plugins/maps/public/routes/list_page/maps_list_view.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, memo } from 'react'; +import React, { useCallback, memo, useEffect } from 'react'; import type { SavedObjectsFindOptionsReference, ScopedHistory } from '@kbn/core/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { i18n } from '@kbn/i18n'; @@ -72,8 +72,14 @@ function MapsListViewComp({ history }: Props) { const listingLimit = getUiSettings().get(SAVED_OBJECTS_LIMIT_SETTING); const initialPageSize = getUiSettings().get(SAVED_OBJECTS_PER_PAGE_SETTING); - getCoreChrome().docTitle.change(APP_NAME); - getCoreChrome().setBreadcrumbs([{ text: APP_NAME }]); + // TLDR; render should be side effect free + // + // setBreadcrumbs fires observables which cause state changes in ScreenReaderRouteAnnouncements. + // wrap chrome updates in useEffect to avoid potentially causing state changes in other component during render phase. + useEffect(() => { + getCoreChrome().docTitle.change(APP_NAME); + getCoreChrome().setBreadcrumbs([{ text: APP_NAME }]); + }, []); const findMaps = useCallback( async ( diff --git a/x-pack/plugins/ml/common/constants/search.ts b/x-pack/plugins/ml/common/constants/search.ts index 9985b502b085c..8ff9b022c274f 100644 --- a/x-pack/plugins/ml/common/constants/search.ts +++ b/x-pack/plugins/ml/common/constants/search.ts @@ -14,8 +14,3 @@ export const SEARCH_QUERY_LANGUAGE = { } as const; export type SearchQueryLanguage = typeof SEARCH_QUERY_LANGUAGE[keyof typeof SEARCH_QUERY_LANGUAGE]; - -export interface ErrorMessage { - query: string; - message: string; -} diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index 8d419f120a564..b540c4d3751f1 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -16,7 +16,6 @@ export { export { getSeverityColor, getSeverityType } from './util/anomaly_utils'; export { composeValidators, patternValidator } from './util/validators'; export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils'; -export { extractErrorMessage } from './util/errors'; export type { RuntimeMappings } from './types/fields'; export { getDefaultCapabilities as getDefaultMlCapabilities } from './types/capabilities'; export { DATAFEED_STATE, JOB_STATE } from './constants/states'; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 26bdd29ac3090..cba66124bab4b 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -9,7 +9,7 @@ import Boom from '@hapi/boom'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; -import { EsErrorBody } from '../util/errors'; +import { EsErrorBody } from '@kbn/ml-error-utils'; import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; import type { UrlConfig } from './custom_urls'; diff --git a/x-pack/plugins/ml/common/types/job_service.ts b/x-pack/plugins/ml/common/types/job_service.ts index a3e1571070ffd..cf029111e0577 100644 --- a/x-pack/plugins/ml/common/types/job_service.ts +++ b/x-pack/plugins/ml/common/types/job_service.ts @@ -5,10 +5,10 @@ * 2.0. */ +import type { ErrorType } from '@kbn/ml-error-utils'; import { Job, JobStats, IndicesOptions } from './anomaly_detection_jobs'; import { RuntimeMappings } from './fields'; import { ES_AGGREGATION } from '../constants/aggregation_types'; -import { ErrorType } from '../util/errors'; export interface MlJobsResponse { jobs: Job[]; diff --git a/x-pack/plugins/ml/common/types/job_validation.ts b/x-pack/plugins/ml/common/types/job_validation.ts index 0c1db63ff3762..226166d45c956 100644 --- a/x-pack/plugins/ml/common/types/job_validation.ts +++ b/x-pack/plugins/ml/common/types/job_validation.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ErrorType } from '../util/errors'; +import type { ErrorType } from '@kbn/ml-error-utils'; export interface DatafeedValidationResponse { valid: boolean; diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts index dd9f098cabe1c..2a6cc9bbd57ce 100644 --- a/x-pack/plugins/ml/common/types/modules.ts +++ b/x-pack/plugins/ml/common/types/modules.ts @@ -6,8 +6,8 @@ */ import type { SavedObjectAttributes } from '@kbn/core/types'; +import type { ErrorType } from '@kbn/ml-error-utils'; import type { Datafeed, Job } from './anomaly_detection_jobs'; -import type { ErrorType } from '../util/errors'; export interface ModuleJob { id: string; diff --git a/x-pack/plugins/ml/common/types/results.ts b/x-pack/plugins/ml/common/types/results.ts index cf25fc6081e16..3fda97b5740b1 100644 --- a/x-pack/plugins/ml/common/types/results.ts +++ b/x-pack/plugins/ml/common/types/results.ts @@ -7,7 +7,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { LineAnnotationDatum, RectAnnotationDatum } from '@elastic/charts'; -import type { ErrorType } from '../util/errors'; +import type { ErrorType } from '@kbn/ml-error-utils'; import type { EntityField } from '../util/anomaly_utils'; import type { Datafeed, JobId, ModelSnapshot } from './anomaly_detection_jobs'; import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../constants/aggregation_types'; diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index ab3b97d1e614d..adaf00fd9405f 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ErrorType } from '../util/errors'; +import type { ErrorType } from '@kbn/ml-error-utils'; export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export type TrainedModelType = 'trained-model'; diff --git a/x-pack/plugins/ml/common/util/errors/types.ts b/x-pack/plugins/ml/common/util/errors/types.ts deleted file mode 100644 index 9f4b123e3e45d..0000000000000 --- a/x-pack/plugins/ml/common/util/errors/types.ts +++ /dev/null @@ -1,74 +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 type { IHttpFetchError } from '@kbn/core-http-browser'; -import Boom from '@hapi/boom'; - -export interface EsErrorRootCause { - type: string; - reason: string; - caused_by?: EsErrorRootCause; - script?: string; -} - -export interface EsErrorBody { - error: { - root_cause?: EsErrorRootCause[]; - caused_by?: EsErrorRootCause; - type: string; - reason: string; - }; - status: number; -} - -export interface MLResponseError { - statusCode: number; - error: string; - message: string; - attributes?: { - body: EsErrorBody; - }; -} - -export interface ErrorMessage { - message: string; -} - -export interface MLErrorObject { - causedBy?: string; - message: string; - statusCode?: number; - fullError?: EsErrorBody; -} - -export interface MLHttpFetchErrorBase extends IHttpFetchError { - body: T; -} - -export type MLHttpFetchError = MLHttpFetchErrorBase; - -export type ErrorType = MLHttpFetchError | EsErrorBody | Boom.Boom | string | undefined; - -export function isEsErrorBody(error: any): error is EsErrorBody { - return error && error.error?.reason !== undefined; -} - -export function isErrorString(error: any): error is string { - return typeof error === 'string'; -} - -export function isErrorMessage(error: any): error is ErrorMessage { - return error && error.message !== undefined && typeof error.message === 'string'; -} - -export function isMLResponseError(error: any): error is MLResponseError { - return typeof error.body === 'object' && 'message' in error.body; -} - -export function isBoomError(error: any): error is Boom.Boom { - return error?.isBoom === true; -} diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx new file mode 100644 index 0000000000000..620aabd1c842b --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/custom_time_range_picker.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, { FC, useMemo, useState } from 'react'; +import moment, { type Moment } from 'moment'; +import { + EuiDatePicker, + EuiDatePickerRange, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiIconTip, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; +import { useMlKibana } from '../../../contexts/kibana'; + +interface CustomUrlTimeRangePickerProps { + onCustomTimeRangeChange: (customTimeRange?: { start: Moment; end: Moment }) => void; + customTimeRange?: { start: Moment; end: Moment }; +} + +/* + * React component for the form for adding a custom time range. + */ +export const CustomTimeRangePicker: FC = ({ + onCustomTimeRangeChange, + customTimeRange, +}) => { + const [showCustomTimeRangeSelector, setShowCustomTimeRangeSelector] = useState(false); + const { + services: { + data: { + query: { + timefilter: { timefilter }, + }, + }, + }, + } = useMlKibana(); + + const onCustomTimeRangeSwitchChange = (checked: boolean) => { + if (checked === false) { + // Clear the custom time range so it isn't persisted + onCustomTimeRangeChange(undefined); + } + setShowCustomTimeRangeSelector(checked); + }; + + // If the custom time range is not set, default to the timefilter settings + const currentTimeRange = useMemo( + () => + customTimeRange ?? { + start: moment(timefilter.getAbsoluteTime().from), + end: moment(timefilter.getAbsoluteTime().to), + }, + [customTimeRange, timefilter] + ); + + const handleStartChange = (date: moment.Moment) => { + onCustomTimeRangeChange({ ...currentTimeRange, start: date }); + }; + const handleEndChange = (date: moment.Moment) => { + onCustomTimeRangeChange({ ...currentTimeRange, end: date }); + }; + + const { start, end } = currentTimeRange; + + return ( + <> + + + + + + + + } + > + + } + checked={showCustomTimeRangeSelector} + onChange={(e) => onCustomTimeRangeSwitchChange(e.target.checked)} + compressed + /> + + + + {showCustomTimeRangeSelector ? ( + <> + + + } + > + end} + startDateControl={ + + } + endDateControl={ + + } + /> + + + ) : null} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx index 315c60fab6a6f..523f59c32f224 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_url_editor/editor.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { ChangeEvent, useMemo, useState, useRef, useEffect, FC } from 'react'; +import React, { ChangeEvent, useState, useRef, useEffect, FC } from 'react'; +import { type Moment } from 'moment'; import { EuiComboBox, @@ -29,10 +30,11 @@ import { DataView } from '@kbn/data-views-plugin/public'; import { CustomUrlSettings, isValidCustomUrlSettingsTimeRange } from './utils'; import { isValidLabel } from '../../../util/custom_url_utils'; import { type DataFrameAnalyticsConfig } from '../../../../../common/types/data_frame_analytics'; -import { Job, isAnomalyDetectionJob } from '../../../../../common/types/anomaly_detection_jobs'; +import { type Job } from '../../../../../common/types/anomaly_detection_jobs'; import { TIME_RANGE_TYPE, TimeRangeType, URL_TYPE } from './constants'; import { UrlConfig } from '../../../../../common/types/custom_urls'; +import { CustomTimeRangePicker } from './custom_time_range_picker'; import { useMlKibana } from '../../../contexts/kibana'; import { getDropDownOptions } from './get_dropdown_options'; @@ -66,6 +68,7 @@ interface CustomUrlEditorProps { dashboards: Array<{ id: string; title: string }>; dataViewListItems: DataViewListItem[]; showTimeRangeSelector?: boolean; + showCustomTimeRangeSelector: boolean; job: Job | DataFrameAnalyticsConfig; } @@ -78,10 +81,12 @@ export const CustomUrlEditor: FC = ({ savedCustomUrls, dashboards, dataViewListItems, + showTimeRangeSelector, + showCustomTimeRangeSelector, job, }) => { const [queryEntityFieldNames, setQueryEntityFieldNames] = useState([]); - const isAnomalyJob = useMemo(() => isAnomalyDetectionJob(job), [job]); + const [hasTimefield, setHasTimefield] = useState(false); const { services: { @@ -101,6 +106,9 @@ export const CustomUrlEditor: FC = ({ } catch (e) { dataViewToUse = undefined; } + if (dataViewToUse && dataViewToUse.timeFieldName) { + setHasTimefield(true); + } const dropDownOptions = await getDropDownOptions(isFirst.current, job, dataViewToUse); setQueryEntityFieldNames(dropDownOptions); @@ -132,6 +140,13 @@ export const CustomUrlEditor: FC = ({ }); }; + const onCustomTimeRangeChange = (timeRange?: { start: Moment; end: Moment }) => { + setEditCustomUrl({ + ...customUrl, + customTimeRange: timeRange, + }); + }; + const onDashboardChange = (e: ChangeEvent) => { const kibanaSettings = customUrl.kibanaSettings; setEditCustomUrl({ @@ -345,58 +360,66 @@ export const CustomUrlEditor: FC = ({ /> )} + {type === URL_TYPE.KIBANA_DASHBOARD || + (type === URL_TYPE.KIBANA_DISCOVER && showCustomTimeRangeSelector && hasTimefield) ? ( + + ) : null} - {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && isAnomalyJob && ( - <> - - - - - } - className="url-time-range" - display="rowCompressed" - > - - - - {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( - + {(type === URL_TYPE.KIBANA_DASHBOARD || type === URL_TYPE.KIBANA_DISCOVER) && + showTimeRangeSelector && ( + <> + + + } className="url-time-range" - error={invalidIntervalError} - isInvalid={isInvalidTimeRange} display="rowCompressed" > - - )} - - - )} + {timeRange.type === TIME_RANGE_TYPE.INTERVAL && ( + + + } + className="url-time-range" + error={invalidIntervalError} + isInvalid={isInvalidTimeRange} + display="rowCompressed" + > + + + + )} + + + )} {type === URL_TYPE.OTHER && ( { // Get the complete list of attributes for the selected dashboard (query, filters). const { dashboardId, queryFieldNames } = settings.kibanaSettings ?? {}; @@ -253,11 +269,13 @@ async function buildDashboardUrlFromSettings(settings: CustomUrlSettings): Promi const dashboard = getDashboard(); + const { from, to } = getUrlRangeFromSettings(settings); + const location = await dashboard?.locator?.getLocation({ dashboardId, timeRange: { - from: '$earliest$', - to: '$latest$', + from, + to, mode: 'absolute', }, filters, @@ -299,10 +317,12 @@ function buildDiscoverUrlFromSettings(settings: CustomUrlSettings) { // Add time settings to the global state URL parameter with $earliest$ and // $latest$ tokens which get substituted for times around the time of the // anomaly on which the URL will be run against. + const { from, to } = getUrlRangeFromSettings(settings); + const _g = rison.encode({ time: { - from: '$earliest$', - to: '$latest$', + from, + to, mode: 'absolute', }, }); diff --git a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx index 9d3db04fa40de..4f9ad5245cf91 100644 --- a/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx +++ b/x-pack/plugins/ml/public/application/components/custom_urls/custom_urls.tsx @@ -42,6 +42,8 @@ import { import { openCustomUrlWindow } from '../../util/custom_url_utils'; import { UrlConfig } from '../../../../common/types/custom_urls'; import type { CustomUrlsWrapperProps } from './custom_urls_wrapper'; +import { isAnomalyDetectionJob } from '../../../../common/types/anomaly_detection_jobs'; +import { isDataFrameAnalyticsConfigs } from '../../../../common/types/data_frame_analytics'; const MAX_NUMBER_DASHBOARDS = 1000; @@ -206,6 +208,8 @@ class CustomUrlsUI extends Component { const editMode = this.props.editMode ?? 'inline'; const editor = ( = ({ // The internal state of the input query bar updated on every key stroke. const [searchInput, setSearchInput] = useState(query); const [idToSelectedMap, setIdToSelectedMap] = useState<{ [id: string]: boolean }>({}); - const [errorMessage, setErrorMessage] = useState(undefined); + const [queryErrorMessage, setQueryErrorMessage] = useState( + undefined + ); const { services } = useMlKibana(); const { @@ -119,7 +118,7 @@ export const ExplorationQueryBar: FC = ({ convertedQuery = luceneStringToDsl(query.query as string); break; default: - setErrorMessage({ + setQueryErrorMessage({ query: query.query as string, message: i18n.translate('xpack.ml.queryBar.queryLanguageNotSupported', { defaultMessage: 'Query language is not supported', @@ -133,7 +132,7 @@ export const ExplorationQueryBar: FC = ({ language: query.language, }); } catch (e) { - setErrorMessage({ query: query.query as string, message: e.message }); + setQueryErrorMessage({ query: query.query as string, message: e.message }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [query.query]); @@ -187,7 +186,7 @@ export const ExplorationQueryBar: FC = ({ return ( setErrorMessage(undefined)} + closePopover={() => setQueryErrorMessage(undefined)} input={ @@ -249,14 +248,14 @@ export const ExplorationQueryBar: FC = ({ )} } - isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + isOpen={queryErrorMessage?.query === searchInput.query && queryErrorMessage?.message !== ''} > {i18n.translate('xpack.ml.stepDefineForm.invalidQuery', { defaultMessage: 'Invalid Query', })} {': '} - {errorMessage?.message.split('\n')[0]} + {queryErrorMessage?.message.split('\n')[0]} ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index b52c06905792c..30749558a23a1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -10,9 +10,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; import { CoreSetup } from '@kbn/core/public'; - import { i18n } from '@kbn/i18n'; import type { DataView } from '@kbn/data-views-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { MlApiServices } from '../../../../../services/ml_api_service'; import { DataLoader } from '../../../../../datavisualizer/index_based/data_loader'; @@ -35,7 +36,6 @@ import { FEATURE_IMPORTANCE, TOP_CLASSES } from '../../../../common/constants'; import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics'; import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; import { isRegressionAnalysis } from '../../../../common/analytics'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { useTrainedModelsApiService } from '../../../../../services/ml_api_service/trained_models'; import { FeatureImportanceBaseline } from '../../../../../../../common/types/feature_importance'; import { useExplorationDataGrid } from './use_exploration_data_grid'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index 76b85e2384dfd..0ba51a7ca64da 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -11,6 +11,7 @@ import { cloneDeep, isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; @@ -19,7 +20,6 @@ import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; import { DataFrameAnalyticsListRow } from '../analytics_list/common'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; interface PropDefinition { /** diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx index 4d1565c1769f3..01b6e3a3f50ad 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { useMlKibana } from '../../../../../contexts/kibana'; import { useToastNotificationService } from '../../../../../services/toast_notification_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index cddc4fcd092dc..4a6ed2176be25 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -10,7 +10,8 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; import { DuplicateDataViewError } from '@kbn/data-plugin/public'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index 537b2016d9af3..c11490a660f10 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -6,7 +6,8 @@ */ import { i18n } from '@kbn/i18n'; -import { extractErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { ml } from '../../../../../services/ml_api_service'; import { ToastNotificationService } from '../../../../../services/toast_notification_service'; import { refreshAnalyticsList$, REFRESH_ANALYTICS_LIST_STATE } from '../../../../common'; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx index bbb18697dab1c..8f4cd30a4c115 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_query_bar/explorer_query_bar.tsx @@ -12,7 +12,8 @@ import { fromKueryExpression, luceneStringToDsl, toElasticsearchQuery } from '@k import type { Query } from '@kbn/es-query'; import { QueryStringInput } from '@kbn/unified-search-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; -import { SEARCH_QUERY_LANGUAGE, ErrorMessage } from '../../../../../common/constants/search'; +import type { QueryErrorMessage } from '@kbn/ml-error-utils'; +import { SEARCH_QUERY_LANGUAGE } from '../../../../../common/constants/search'; import { InfluencersFilterQuery } from '../../../../../common/types/es_client'; import { useAnomalyExplorerContext } from '../../anomaly_explorer_context'; import { useMlKibana } from '../../../contexts/kibana'; @@ -129,7 +130,9 @@ export const ExplorerQueryBar: FC = ({ const [searchInput, setSearchInput] = useState( getInitSearchInputState({ filterActive, queryString }) ); - const [errorMessage, setErrorMessage] = useState(undefined); + const [queryErrorMessage, setQueryErrorMessage] = useState( + undefined + ); useEffect( function updateSearchInputFromFilter() { @@ -160,14 +163,14 @@ export const ExplorerQueryBar: FC = ({ } } catch (e) { console.log('Invalid query syntax in search bar', e); // eslint-disable-line no-console - setErrorMessage({ query: query.query as string, message: e.message }); + setQueryErrorMessage({ query: query.query as string, message: e.message }); } }; return ( = ({ }} /> } - isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + isOpen={queryErrorMessage?.query === searchInput.query && queryErrorMessage?.message !== ''} > {i18n.translate('xpack.ml.explorer.invalidKuerySyntaxErrorMessageQueryBar', { defaultMessage: 'Invalid query', })} {': '} - {errorMessage?.message.split('\n')[0]} + {queryErrorMessage?.message.split('\n')[0]} ); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts index e7ad6802c4401..f0f26a3918438 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_utils.ts @@ -11,19 +11,20 @@ import { get, union, uniq } from 'lodash'; import moment from 'moment-timezone'; +import { lastValueFrom } from 'rxjs'; + import { ES_FIELD_TYPES } from '@kbn/field-types'; import { asyncForEach } from '@kbn/std'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import type { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; -import { lastValueFrom } from 'rxjs'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE, ANOMALIES_TABLE_DEFAULT_QUERY_SIZE, } from '../../../common/constants/search'; import { EntityField, getEntityFieldList } from '../../../common/util/anomaly_utils'; import { getDataViewIdFromName } from '../util/index_utils'; -import { extractErrorMessage } from '../../../common/util/errors'; import { ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types'; import { isSourceDataChartableForDetector, diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx index 8b3ed99709cb5..86cd23b46383e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/job_messages_pane.tsx @@ -9,10 +9,10 @@ import React, { FC, useCallback, useEffect, useState } from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { ml } from '../../../../services/ml_api_service'; import { JobMessages } from '../../../../components/job_messages'; import { JobMessage } from '../../../../../../common/types/audit_message'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; import { useToastNotificationService } from '../../../../services/toast_notification_service'; import { useMlApiContext } from '../../../../contexts/kibana'; import { checkPermission } from '../../../../capabilities/check_capabilities'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts index 159f1af10e69f..52d4d77106217 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; import { combineLatest, Observable, of, Subject, Subscription } from 'rxjs'; import { isEqual, cloneDeep } from 'lodash'; import { @@ -21,10 +20,13 @@ import { skipWhile, } from 'rxjs/operators'; import { useEffect, useMemo } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { type MLHttpFetchError, extractErrorMessage } from '@kbn/ml-error-utils'; + import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../../../../../common/constants/new_job'; import { ml } from '../../../../../services/ml_api_service'; import { JobValidator, VALIDATION_DELAY_MS } from '../../job_validator/job_validator'; -import { MLHttpFetchError, extractErrorMessage } from '../../../../../../../common/util/errors'; import { useMlKibana } from '../../../../../contexts/kibana'; import { JobCreator } from '../job_creator'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts index a22b6d9fd57a5..f01898fa905a1 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_dashboard/quick_create_job_base.ts @@ -17,9 +17,9 @@ import type { Filter, Query, DataViewBase } from '@kbn/es-query'; import { FilterStateStore } from '@kbn/es-query'; import type { Embeddable } from '@kbn/lens-plugin/public'; import type { MapEmbeddable } from '@kbn/maps-plugin/public'; +import type { ErrorType } from '@kbn/ml-error-utils'; import type { MlApiServices } from '../../../services/ml_api_service'; import { getFiltersForDSLQuery } from '../../../../../common/util/job_utils'; -import type { ErrorType } from '../../../../../common/util/errors'; import { CREATED_BY_LABEL } from '../../../../../common/constants/new_job'; import { createQueries } from '../utils/new_job_utils'; import { createDatafeedId } from '../../../../../common/util/job_utils'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts index c7f5a02e75d5b..4552123184f5c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/job_from_lens/visualization_extractor.ts @@ -10,8 +10,8 @@ import { layerTypes } from '@kbn/lens-plugin/public'; import { i18n } from '@kbn/i18n'; +import type { ErrorType } from '@kbn/ml-error-utils'; import { JOB_TYPE } from '../../../../../common/constants/new_job'; -import { ErrorType } from '../../../../../common/util/errors'; import { getVisTypeFactory, isCompatibleLayer, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx index 71d68b895f605..9e68d9f6f8c37 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx @@ -6,7 +6,6 @@ */ import React, { FC, useState, useEffect, useCallback, useContext } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { @@ -23,7 +22,10 @@ import { EuiModalBody, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import { SavedObjectFinder } from '@kbn/saved-objects-finder-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { JobCreatorContext } from '../../../job_creator_context'; import { AdvancedJobCreator } from '../../../../../common/job_creator'; import { resetAdvancedJob } from '../../../../../common/job_creator/util/general'; @@ -31,7 +33,6 @@ import { CombinedJob, Datafeed, } from '../../../../../../../../../common/types/anomaly_detection_jobs'; -import { extractErrorMessage } from '../../../../../../../../../common/util/errors'; import type { DatafeedValidationResponse } from '../../../../../../../../../common/types/job_validation'; import { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx index 8030292bd9e59..090fcf40d3f17 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/category_stopped_partitions.tsx @@ -11,10 +11,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { from } from 'rxjs'; import { switchMap, takeWhile, tap } from 'rxjs/operators'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { JobCreatorContext } from '../../../job_creator_context'; import { CategorizationJobCreator } from '../../../../../common/job_creator'; import { ml } from '../../../../../../../services/ml_api_service'; -import { extractErrorProperties } from '../../../../../../../../../common/util/errors'; const NUMBER_OF_PREVIEW = 5; export const CategoryStoppedPartitions: FC = () => { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx index 4491dfab1abdd..b12b4261a0628 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/top_categories.tsx @@ -8,13 +8,13 @@ import React, { FC, useContext, useEffect, useState } from 'react'; import { EuiBasicTable, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; import { JobCreatorContext } from '../../../job_creator_context'; import { CategorizationJobCreator } from '../../../../../common/job_creator'; import { Results } from '../../../../../common/results_loader'; import { ml } from '../../../../../../../services/ml_api_service'; import { useToastNotificationService } from '../../../../../../../services/toast_notification_service'; import { NUMBER_OF_CATEGORY_EXAMPLES } from '../../../../../../../../../common/constants/categorization_job'; -import { extractErrorProperties } from '../../../../../../../../../common/util/errors'; export const TopCategories: FC = () => { const { displayErrorToast } = useToastNotificationService(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index 7fb483b54cf5d..75bc8772e0ac6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -6,12 +6,15 @@ */ import React, { FC, Fragment, useContext, useState } from 'react'; + import { EuiButton, EuiFlexItem } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { JobRunner } from '../../../../../common/job_runner'; import { useMlKibana } from '../../../../../../../contexts/kibana'; -import { extractErrorMessage } from '../../../../../../../../../common/util/errors'; import { JobCreatorContext } from '../../../job_creator_context'; import { DATAFEED_STATE } from '../../../../../../../../../common/constants/states'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index 93b6086398358..cd6bd11096c65 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -18,11 +18,11 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { ModuleJobUI } from '../page'; import { SETUP_RESULTS_WIDTH } from './module_jobs'; import { tabColor } from '../../../../../../common/util/group_color_utils'; import { JobOverride, DatafeedResponse } from '../../../../../../common/types/modules'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; interface JobItemProps { job: ModuleJobUI; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx index 079e698c3a5c0..95ac0b4043f57 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx @@ -18,8 +18,8 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { KibanaObjectUi } from '../page'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; export interface KibanaObjectItemProps { objectType: string; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts index 59fa138d0f29f..1e3e3ecd5dfd7 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_base.ts @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { map } from 'rxjs/operators'; import { SupportedPytorchTasksType } from '@kbn/ml-trained-models-utils'; -import { MLHttpFetchError } from '../../../../../common/util/errors'; +import type { MLHttpFetchError } from '@kbn/ml-error-utils'; import { trainedModelsApiProvider } from '../../../services/ml_api_service/trained_models'; import { getInferenceInfoComponent } from './inference_info'; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx index 5060f0033fd6c..4dbe900283657 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/index_input.tsx @@ -8,7 +8,7 @@ import React, { FC, useState, useMemo, useCallback, FormEventHandler } from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { FormattedMessage } from '@kbn/i18n-react'; + import { EuiSpacer, EuiButton, @@ -21,8 +21,10 @@ import { EuiForm, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { ErrorMessage } from '../../inference_error'; -import { extractErrorMessage } from '../../../../../../common'; import type { InferrerType } from '..'; import { useIndexInput, InferenceInputFormIndexControls } from '../index_input'; import { RUNNING_STATE } from '../inference_base'; diff --git a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx index 3446bda477b4a..fc162a305c32b 100644 --- a/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx +++ b/x-pack/plugins/ml/public/application/model_management/test_models/models/inference_input_form/text_input.tsx @@ -6,13 +6,14 @@ */ import React, { FC, useState, useMemo, useCallback, FormEventHandler } from 'react'; - import useObservable from 'react-use/lib/useObservable'; -import { FormattedMessage } from '@kbn/i18n-react'; + import { EuiSpacer, EuiButton, EuiTabs, EuiTab, EuiForm } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { ErrorMessage } from '../../inference_error'; -import { extractErrorMessage } from '../../../../../../common'; import type { InferrerType } from '..'; import { OutputLoadingContent } from '../../output_loading'; import { RUNNING_STATE } from '../inference_base'; diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 2cfcf50c56514..c24ae573a9514 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -16,6 +16,7 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { each, get } from 'lodash'; import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +import type { ErrorType } from '@kbn/ml-error-utils'; import { Dictionary } from '../../../../common/types/common'; import { ML_MEDIAN_PERCENTS } from '../../../../common/util/job_utils'; import { Datafeed, JobId } from '../../../../common/types/anomaly_detection_jobs'; @@ -28,7 +29,6 @@ import { ES_AGGREGATION } from '../../../../common/constants/aggregation_types'; import { InfluencersFilterQuery } from '../../../../common/types/es_client'; import { RecordForInfluencer } from './results_service'; import { isRuntimeMappings } from '../../../../common'; -import { ErrorType } from '../../../../common/util/errors'; export interface ResultResponse { success: boolean; diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts index 8bc7bc8c87e35..585b881010d7a 100644 --- a/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts +++ b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts @@ -8,13 +8,9 @@ import { i18n } from '@kbn/i18n'; import { ToastInput, ToastOptions, ToastsStart } from '@kbn/core/public'; import { useMemo } from 'react'; +import { extractErrorProperties, type ErrorType, MLRequestFailure } from '@kbn/ml-error-utils'; import { getToastNotifications } from '../../util/dependency_cache'; import { useNotifications } from '../../contexts/kibana'; -import { - ErrorType, - extractErrorProperties, - MLRequestFailure, -} from '../../../../common/util/errors'; export type ToastNotificationService = ReturnType; diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js index e97cf8e2639fc..a767d7b65e4f3 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js @@ -5,10 +5,11 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { getToastNotifications } from '../../../util/dependency_cache'; import { ml } from '../../../services/ml_api_service'; -import { i18n } from '@kbn/i18n'; -import { extractErrorMessage } from '../../../../../common/util/errors'; export async function deleteCalendars(calendarsToDelete, callback) { if (calendarsToDelete === undefined || calendarsToDelete.length === 0) { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js index 64bc1eefccd79..3342680070e8b 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/forecasting_modal.js @@ -16,9 +16,13 @@ import React, { Component } from 'react'; import { EuiButton, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { withKibana } from '@kbn/kibana-react-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { FORECAST_REQUEST_STATE, JOB_STATE } from '../../../../../common/constants/states'; import { MESSAGE_LEVEL } from '../../../../../common/constants/message_levels'; -import { extractErrorMessage } from '../../../../../common/util/errors'; import { isJobVersionGte } from '../../../../../common/util/job_utils'; import { parseInterval } from '../../../../../common/util/parse_interval'; import { Modal } from './modal'; @@ -26,9 +30,6 @@ import { PROGRESS_STATES } from './progress_states'; import { ml } from '../../../services/ml_api_service'; import { mlJobService } from '../../../services/job_service'; import { mlForecastService } from '../../../services/forecast_service'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { withKibana } from '@kbn/kibana-react-plugin/public'; export const FORECAST_DURATION_MAX_DAYS = 3650; // Max forecast duration allowed by analytics. diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx index c9ca8250bedd9..af42229d8ac79 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseries_chart/timeseries_chart_with_tooltip.tsx @@ -7,11 +7,11 @@ import React, { FC, useEffect, useState, useCallback, useContext } from 'react'; import { i18n } from '@kbn/i18n'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { MlTooltipComponent } from '../../../components/chart_tooltip'; import { TimeseriesChart } from './timeseries_chart'; import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../../common/constants/search'; -import { extractErrorMessage } from '../../../../../common/util/errors'; import { Annotation } from '../../../../../common/types/annotations'; import { useMlKibana, useNotifications } from '../../../contexts/kibana'; import { getBoundsRoundedToInterval } from '../../../util/time_buckets'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index f9dd1fa94c4f0..7a7837ba71435 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -7,9 +7,9 @@ import { forkJoin, Observable, of } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { ml } from '../../services/ml_api_service'; import { ANNOTATIONS_TABLE_DEFAULT_QUERY_SIZE } from '../../../../common/constants/search'; -import { extractErrorMessage } from '../../../../common/util/errors'; import { mlTimeSeriesSearchService } from '../timeseries_search_service'; import { mlResultsService, CriteriaField } from '../../services/results_service'; import { Job } from '../../../../common/types/anomaly_detection_jobs'; diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx index 7585ffe32118d..e2fff3bd286cf 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/common/job_details.tsx @@ -5,14 +5,10 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; -import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import React, { FC, useState, useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import useDebounce from 'react-use/lib/useDebounce'; -import type { Embeddable } from '@kbn/lens-plugin/public'; -import type { MapEmbeddable } from '@kbn/maps-plugin/public'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiFlexGroup, EuiFlexItem, @@ -30,11 +26,16 @@ import { EuiCallOut, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { Embeddable } from '@kbn/lens-plugin/public'; +import type { MapEmbeddable } from '@kbn/maps-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; + import { QuickLensJobCreator } from '../../../application/jobs/new_job/job_from_lens'; import type { LayerResult } from '../../../application/jobs/new_job/job_from_lens'; import type { CreateState } from '../../../application/jobs/new_job/job_from_dashboard'; import { JOB_TYPE, DEFAULT_BUCKET_SPAN } from '../../../../common/constants/new_job'; -import { extractErrorMessage } from '../../../../common/util/errors'; import { basicJobValidation } from '../../../../common/util/job_utils'; import { JOB_ID_MAX_LENGTH } from '../../../../common/constants/validation'; import { invalidTimeIntervalMessage } from '../../../application/jobs/new_job/common/job_validator/util'; diff --git a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx index f012e928e6b61..5f804e209eaa3 100644 --- a/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx +++ b/x-pack/plugins/ml/public/embeddables/job_creation/lens/lens_vis_layer_selection_flyout/layer/incompatible_layer.tsx @@ -6,13 +6,13 @@ */ import React, { FC } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText } from '@elastic/eui'; -import type { LayerResult } from '../../../../../application/jobs/new_job/job_from_lens'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; -import { extractErrorMessage } from '../../../../../../common/util/errors'; +import type { LayerResult } from '../../../../../application/jobs/new_job/job_from_lens'; interface Props { layer: LayerResult; diff --git a/x-pack/plugins/ml/public/shared.ts b/x-pack/plugins/ml/public/shared.ts index f247be1004718..1d19fee5c8392 100644 --- a/x-pack/plugins/ml/public/shared.ts +++ b/x-pack/plugins/ml/public/shared.ts @@ -15,7 +15,6 @@ export * from '../common/types/modules'; export * from '../common/types/audit_message'; export * from '../common/util/anomaly_utils'; -export * from '../common/util/errors'; export * from '../common/util/validators'; export * from '../common/util/date_utils'; diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts index 6d4982ecbf464..9d286738b17da 100644 --- a/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/validation.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient } from '@kbn/core/server'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { getAnalysisType } from '../../../common/util/analytics_utils'; import { ANALYSIS_CONFIG_TYPE } from '../../../common/constants/data_frame_analytics'; import { @@ -25,7 +26,6 @@ import { isRegressionAnalysis, isClassificationAnalysis, } from '../../../common/util/analytics_utils'; -import { extractErrorMessage } from '../../../common/util/errors'; import { AnalysisConfig, DataFrameAnalyticsConfig, diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json index b3395d82a9c29..d600e4a637acf 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/manifest.json @@ -2,7 +2,7 @@ "id": "security_auth", "title": "Security: Authentication", "description": "Detect anomalous activity in your ECS-compatible authentication logs.", - "type": "auth data", + "type": "Auth data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*,filebeat-*,winlogbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json index 7ca7a5ebd71e4..ac50e2f53535c 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon events", + "detector_description": "Detects high count of logon events.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events" + "security_app_display_name": "Spike in Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json index 47096f4c6413f..d23f8df88ef6a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_events_for_a_source_ip.json @@ -1,25 +1,17 @@ { "description": "Security: Authentication - Looks for an unusually large spike in successful authentication events from a particular source IP address. This can be due to password spraying, user enumeration, or brute force activity.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of auth events for a source IP", + "detector_description": "Detects high count of auth events for a source IP.", "function": "high_non_zero_count", "by_field_name": "source.ip", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "winlog.event_data.LogonType", - "user.name" - ], - "model_prune_window": "30d" + "influencers": ["source.ip", "winlog.event_data.LogonType", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Logon Events from a Source IP" + "security_app_display_name": "Spike in Logon Events from a Source IP", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json index 48586ef642ca6..db2db5ea00832 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_high_count_logon_fails.json @@ -1,20 +1,16 @@ { "description": "Security: Authentication - Looks for an unusually large spike in authentication failure events. This can be due to password spraying, user enumeration, or brute force activity and may be a precursor to account takeover or credentialed access.", - "groups": [ - "security", - "authentication" - ], + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high count of logon fails", + "detector_description": "Detects high count of logon fails.", "function": "high_non_zero_count", "detector_index": 0 } ], - "influencers": [], - "model_prune_window": "30d" + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -25,6 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Spike in Failed Logon Events" + "security_app_display_name": "Spike in Failed Logon Events", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json index 1f421ed298b9f..57477497aeb62 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_hour_for_a_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in at a time of day that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different time zones. In addition, unauthorized user activity often takes place during non-business hours.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare hour for a user", + "detector_description": "Detects rare hour for a user.", "function": "time_of_day", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Hour for a User to Logon" + "security_app_display_name": "Unusual Hour for a User to Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json index 98a249074a67a..81185ef5039c7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_source_ip_for_a_user.json @@ -1,24 +1,18 @@ { - "description": "Security: Authentication - looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for a user logging in from an IP address that is unusual for the user. This can be due to credentialed access via a compromised account when the user and the threat actor are in different locations. An unusual source IP address for a username could also be due to lateral movement when a compromised account is used to pivot between hosts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare source IP for a user", + "detector_description": "Detects rare source IP for a user.", "function": "rare", "by_field_name": "source.ip", "partition_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Unusual Source IP for a User to Logon from" + "security_app_display_name": "Unusual Source IP for a User to Logon from", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json index e2488480e61d1..58530fe085014 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/auth_rare_user.json @@ -1,23 +1,17 @@ { - "description": "Security: Authentication - looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", - "groups": [ - "security", - "authentication" - ], + "description": "Security: Authentication - Looks for an unusual user name in the authentication logs. An unusual user name is one way of detecting credentialed access by means of a new or dormant user account. A user account that is normally inactive, because the user has left the organization, which becomes active, may be due to credentialed access using a compromised account password. Threat actors will sometimes also create new users as a means of persisting in a compromised web application.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare user", + "detector_description": "Detects rare user authentication.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "source.ip", - "user.name" - ] + "influencers": ["source.ip", "user.name", "host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-auth", - "security_app_display_name": "Rare User Logon" + "security_app_display_name": "Rare User Logon", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json index 386b9fab25667..59a9129e7b7bf 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/datafeed_suspicious_login_activity.json @@ -1,15 +1,10 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": { "event.category": "authentication" }}, - {"term": { "agent.type": "auditbeat" }} - ] + "filter": [{ "term": { "event.category": "authentication" } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json index 00e810b5348e7..bbe420b3ec0eb 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_auth/ml/suspicious_login_activity.json @@ -1,24 +1,17 @@ { - "description": "Security: Auditbeat - Detect unusually high number of authentication attempts.", - "groups": [ - "security", - "auditbeat", - "authentication" - ], + "description": "Security: Authentication - Detects unusually high number of authentication attempts.", + "groups": ["security", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high number of authentication attempts", + "detector_description": "Detects high number of authentication attempts for a host.", "function": "high_non_zero_count", - "partition_field_name": "host.name" + "partition_field_name": "host.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "source.ip" - ], + "influencers": ["host.name", "user.name", "source.ip"], "model_prune_window": "30d" }, "allow_lazy_open": true, @@ -31,11 +24,7 @@ "custom_settings": { "created_by": "ml-module-security-auth", "security_app_display_name": "Unusual Login Activity", - "custom_urls": [ - { - "url_name": "IP Address Details", - "url_value": "security/network/ml-network/ip/$source.ip$?_g=()&query=!n&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ] + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json index 93797b9e3e758..52b406a0da7cb 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/manifest.json @@ -1,16 +1,14 @@ { "id": "security_cloudtrail", "title": "Security: Cloudtrail", - "description": "Detect suspicious activity recorded in your cloudtrail logs.", - "type": "Filebeat data", + "description": "Detect suspicious activity recorded in Cloudtrail logs.", + "type": "Cloudtrail data", "logoFile": "logo.json", - "defaultIndexPattern": "filebeat-*", + "defaultIndexPattern": "logs-*,filebeat-*", "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "aws.cloudtrail"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "event.dataset": "aws.cloudtrail" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json index 11b5f4625a484..2ba7c4fdf4085 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/high_distinct_count_error_message.json @@ -1,24 +1,17 @@ { "description": "Security: Cloudtrail - Looks for a spike in the rate of an error message which may simply indicate an impending service failure but these can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_distinct_count(\"aws.cloudtrail.error_message\")", + "detector_description": "Detects high distinct count of Cloudtrail error messages.", "function": "high_distinct_count", - "field_name": "aws.cloudtrail.error_message" + "field_name": "aws.cloudtrail.error_message", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ], - "model_prune_window": "30d" + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Spike in AWS Error Messages" + "security_app_display_name": "Spike in AWS Error Messages", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json index c54c8e8378f2c..7752430876e3f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_error_code.json @@ -1,23 +1,17 @@ { "description": "Security: Cloudtrail - Looks for unusual errors. Rare and unusual errors may simply indicate an impending service failure but they can also be byproducts of attempted or successful persistence, privilege escalation, defense evasion, discovery, lateral movement, or collection activity by a threat actor.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"aws.cloudtrail.error_code\"", + "detector_description": "Detects rare Cloudtrail error codes.", "function": "rare", - "by_field_name": "aws.cloudtrail.error_code" + "by_field_name": "aws.cloudtrail.error_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,6 +22,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Rare AWS Error Code" + "security_app_display_name": "Rare AWS Error Code", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json index 2ed28884be94f..f7be6fe8cc8d7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_city.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (city) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.city_name\"", + "detector_description": "Detects rare event actions for a city.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.city_name" + "partition_field_name": "source.geo.city_name", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.city_name" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.city_name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual City for an AWS Command" + "security_app_display_name": "Unusual City for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json index 1f14357e73444..d73f51f34de3a 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_country.json @@ -1,24 +1,18 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a geolocation (country) that is unusual. This can be the result of compromised credentials or keys.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"source.geo.country_iso_code\"", + "detector_description": "Detects rare event actions for an ISO code.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "source.geo.country_iso_code" + "partition_field_name": "source.geo.country_iso_code", + "detector_index": 0 } ], - "influencers": [ - "aws.cloudtrail.user_identity.arn", - "source.ip", - "source.geo.country_iso_code" - ] + "influencers": ["aws.cloudtrail.user_identity.arn", "source.ip", "source.geo.country_iso_code"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,6 +23,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual Country for an AWS Command" + "security_app_display_name": "Unusual Country for an AWS Command", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json index 76cce7fb829ca..a508028619833 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_cloudtrail/ml/rare_method_for_a_username.json @@ -1,23 +1,22 @@ { "description": "Security: Cloudtrail - Looks for AWS API calls that, while not inherently suspicious or abnormal, are sourcing from a user context that does not normally call the method. This can be the result of compromised credentials or keys as someone uses a valid account to persist, move laterally, or exfil data.", - "groups": [ - "security", - "cloudtrail" - ], + "groups": ["security", "cloudtrail"], "analysis_config": { "bucket_span": "60m", "detectors": [ { - "detector_description": "rare by \"event.action\" partition by \"user.name\"", + "detector_description": "Detects rare event actions for a user.", "function": "rare", "by_field_name": "event.action", - "partition_field_name": "user.name" + "partition_field_name": "user.name", + "detector_index": 0 } ], "influencers": [ "user.name", "source.ip", - "source.geo.city_name" + "source.geo.city_name", + "aws.cloudtrail.user_identity.arn" ] }, "allow_lazy_open": true, @@ -29,6 +28,8 @@ }, "custom_settings": { "created_by": "ml-module-security-cloudtrail", - "security_app_display_name": "Unusual AWS Command for a User" + "security_app_display_name": "Unusual AWS Command for a User", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json index 269f90dea4471..cfff61e304c0e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/manifest.json @@ -2,7 +2,7 @@ "id": "security_linux_v3", "title": "Security: Linux", "description": "Anomaly detection jobs for Linux host-based threat hunting and detection.", - "type": "linux data", + "type": "Linux data", "logoFile": "logo.json", "defaultIndexPattern": "auditbeat-*,logs-*", "query": { @@ -43,10 +43,7 @@ ], "must_not": { "terms": { - "_tier": [ - "data_frozen", - "data_cold" - ] + "_tier": ["data_frozen", "data_cold"] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json index 29f6bf1d98412..b276bcc7856ba 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "network", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", - "by_field_name": "process.name" + "by_field_name": "process.name", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'process.name%20:%20%22$process.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Activity" + "security_app_display_name": "Unusual Linux Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json index 34b97358260ac..a551d6c2c204f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_network_port_activity.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for unusual destination port activity that could indicate command-and-control, persistence mechanism, or data exfiltration activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "network" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare destination.port values.", + "detector_description": "Detects rare destination ports.", "function": "rare", - "by_field_name": "destination.port" + "by_field_name": "destination.port", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Port Activity" + "security_app_display_name": "Unusual Linux Network Port Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json index a20a508391fb9..dea5fa3a5db31 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_process_all_hosts.json @@ -1,65 +1,30 @@ { "description": "Security: Linux - Looks for processes that are unusual to all Linux hosts. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "512mb", - "categorization_examples_limit": 4 - + "model_memory_limit": "512mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Linux Population" + "security_app_display_name": "Anomalous Process for a Linux Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json index 72be89bd79aad..05d46860b145f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_anomalous_user_name.json @@ -1,64 +1,30 @@ { "description": "Security: Linux - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Username" + "security_app_display_name": "Unusual Linux Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json index 1481b7a03a559..fccfa9493e8c2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_configuration_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network configuration discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network configuration discovery to increase their understanding of connected networks and hosts. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "40012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Network Configuration Discovery" + "security_app_display_name": "Unusual Linux Network Configuration Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json index 2b1cf43ac94d3..32dc04c079db1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_network_connection_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system network connection discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network connection discovery to increase their understanding of connected services and systems. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Network Connection Discovery" + "security_app_display_name": "Unusual Linux Network Connection Discovery", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json index fcec32acd69b5..6897876ad6ba3 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_process.json @@ -1,46 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "process.name" - ] + "influencers": ["host.name", "user.name", "process.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux Process Calling the Metadata Service" + "security_app_display_name": "Unusual Linux Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json index d8414c8bf22bd..ad81023d69383 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_metadata_user.json @@ -1,45 +1,30 @@ { "description": "Security: Linux - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4010", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "security_app_display_name": "Unusual Linux User Calling the Metadata Service" + "security_app_display_name": "Unusual Linux User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json index a99e5f95572f7..11be6277c4220 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_sudo_user.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for sudo activity from an unusual user context. Unusual user context changes can be due to privilege escalation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4017", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Sudo Activity" + "security_app_display_name": "Unusual Sudo Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json index 9c8ca5316ace3..08dbbc60d02f7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_rare_user_compiler.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.title", - "host.name", - "process.working_directory", - "user.name" - ] + "influencers": ["process.title", "host.name", "process.working_directory", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4018", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Linux Compiler Activity" + "security_app_display_name": "Anomalous Linux Compiler Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json index 0202854934285..255d0347654b0 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_information_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system information discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system information discovery to gather detailed information about system configuration and software versions. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4014", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Information Discovery Activity" + "security_app_display_name": "Unusual Linux System Information Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json index 23e6e607ccf08..03e57ce2237af 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_process_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system process discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system process discovery to increase their understanding of software applications running on a target host or network. This may be a precursor to the selection of a persistence mechanism or a method of privilege elevation.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4015", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux Process Discovery Activity" + "security_app_display_name": "Unusual Linux Process Discovery Activity", + "managed": true, + "job_revision": 4 } -} \ No newline at end of file +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json index 8659e7a8f1f91..2b1c4dc595777 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_linux_system_user_discovery.json @@ -1,27 +1,17 @@ { "description": "Security: Linux - Looks for commands related to system user or owner discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system owner or user discovery to identify currently active or primary users of a system. This may be a precursor to additional discovery, credential dumping, or privilege elevation activity.", - "groups": [ - "security", - "auditbeat", - "endpoint", - "linux", - "process" - ], + "groups": ["security", "linux"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", - "by_field_name": "user.name" + "by_field_name": "user.name", + "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "process.args", - "user.name" - ] + "influencers": ["process.name", "host.name", "process.args", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "4016", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Linux System Owner or User Discovery Activity" + "security_app_display_name": "Unusual Linux User Discovery Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json index a072007a0f13c..ce0e7f413f676 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_linux/ml/v3_rare_process_by_host_linux.json @@ -1,65 +1,31 @@ { "description": "Security: Linux - Looks for processes that are unusual to a particular Linux host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "auditbeat", - "endpoint", - "linux", - "process", - "security" - ], + "groups": ["linux", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes for a host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { "time_field": "@timestamp", "time_format": "epoch_ms" }, "custom_settings": { - "job_tags": { - "euid": "4002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-linux-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Linux Host" + "security_app_display_name": "Unusual Process for a Linux Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json index bed522d4e954a..edf6c66a213bd 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/manifest.json @@ -2,7 +2,7 @@ "id": "security_network", "title": "Security: Network", "description": "Detect anomalous network activity in your ECS-compatible network logs.", - "type": "network data", + "type": "Network data", "logoFile": "logo.json", "defaultIndexPattern": "logs-*,filebeat-*,packetbeat-*", "query": { @@ -14,7 +14,7 @@ } } ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json index 4479fe8f8c662..b19a3f0e27812 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_by_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network activity to one destination country in the network logs. This could be due to unusually large amounts of reconnaissance or enumeration traffic. Data exfiltration activity may also produce such a surge in traffic to a destination country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_non_zero_count by \"destination.geo.country_name\"", + "detector_description": "Detects high count by country.", "function": "high_non_zero_count", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -19,8 +16,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic to a Country" + "security_app_display_name": "Spike in Network Traffic to a Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json index 984bfea22fa2d..1477e951d3ce9 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_denies.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic that was denied by network ACLs or firewall rules. Such a burst of denied traffic is usually either 1) a misconfigured application or firewall or 2) suspicious or malicious activity. Unsuccessful attempts at network transit, in order to connect to command-and-control (C2), or engage in data exfiltration, may produce a burst of failed connections. This could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network denies.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.port" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Firewall Denies" + "security_app_display_name": "Spike in Firewall Denies", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json index ba740d581a27e..81b516204fbc1 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/high_count_network_events.json @@ -1,14 +1,11 @@ { "description": "Security: Network - Looks for an unusually large spike in network traffic. Such a burst of traffic, if not caused by a surge in business activity, can be due to suspicious or malicious activity. Large-scale data exfiltration may produce a burst of network traffic; this could also be due to unusually large amounts of reconnaissance or enumeration traffic. Denial-of-service attacks or traffic floods may also produce such a surge in traffic.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_count", + "detector_description": "Detects high count of network events.", "function": "high_count", "detector_index": 0 } @@ -18,8 +15,7 @@ "destination.as.organization.name", "source.ip", "destination.ip" - ], - "model_prune_window": "30d" + ] }, "allow_lazy_open": true, "analysis_limits": { @@ -30,6 +26,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Spike in Network Traffic" + "security_app_display_name": "Spike in Network Traffic", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json index 123b802c475fb..4b8799d65b746 100755 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_network/ml/rare_destination_country.json @@ -1,14 +1,11 @@ { "description": "Security: Network - looks for an unusual destination country name in the network logs. This can be due to initial access, persistence, command-and-control, or exfiltration activity. For example, when a user clicks on a link in a phishing email or opens a malicious document, a request may be sent to download and run a payload from a server in a country which does not normally appear in network traffic or business work-flows. Malware instances and persistence mechanisms may communicate with command-and-control (C2) infrastructure in their country of origin, which may be an unusual destination country for the source network.", - "groups": [ - "security", - "network" - ], + "groups": ["security", "network"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"destination.geo.country_name\"", + "detector_description": "Detects rare country names.", "function": "rare", "by_field_name": "destination.geo.country_name", "detector_index": 0 @@ -30,6 +27,8 @@ }, "custom_settings": { "created_by": "ml-module-security-network", - "security_app_display_name": "Network Traffic to Rare Destination Country" + "security_app_display_name": "Network Traffic to Rare Destination Country", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json index f7a65d0137f26..799363b8fbac1 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/manifest.json @@ -1,16 +1,14 @@ { "id": "security_packetbeat", "title": "Security: Packetbeat", - "description": "Detect suspicious network activity in Packetbeat data.", + "description": "Detect suspicious activity in Packetbeat data.", "type": "Packetbeat data", "logoFile": "logo.json", - "defaultIndexPattern": "packetbeat-*", + "defaultIndexPattern": "packetbeat-*,logs-*", "query": { "bool": { - "filter": [ - {"term": {"agent.type": "packetbeat"}} - ], - "must_not": { "terms": { "_tier": [ "data_frozen", "data_cold" ] } } + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "must_not": { "terms": { "_tier": ["data_frozen", "data_cold"] } } } }, "jobs": [ diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json index 449c8af238b56..334435732a07e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_dns_tunneling.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"destination.ip": "169.254.169.254"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "destination.ip": "169.254.169.254" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json index 3a4055eb55ba0..fe87d86ee352f 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_dns_question.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "dns"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "dns" } }, + { "term": { "event.dataset": "network_traffic.dns" } } ], - "must_not": [ - {"bool": {"filter": {"term": {"dns.question.type": "PTR"}}}} - ] + "minimum_should_match": 1, + "must_not": [{ "bool": { "filter": { "term": { "dns.question.type": "PTR" } } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json index 5986c326ea80f..79a297595d8d7 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/datafeed_packetbeat_rare_user_agent.json @@ -1,18 +1,16 @@ { "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], + "indices": ["INDEX_PATTERN_NAME"], "max_empty_searches": 10, "query": { "bool": { - "filter": [ - {"term": {"event.dataset": "http"}}, - {"term": {"agent.type": "packetbeat"}} + "filter": [{ "term": { "agent.type": "packetbeat" } }], + "should": [ + { "term": { "event.dataset": "http" } }, + { "term": { "event.dataset": "network_traffic.http" } } ], - "must_not": [ - {"wildcard": {"user_agent.original": {"value": "Mozilla*"}}} - ] + "minimum_should_match": 1, + "must_not": [{ "wildcard": { "user_agent.original": { "value": "Mozilla*" } } }] } } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json index 313bd8e1bea39..54b8ddf2e7a14 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_dns_tunneling.json @@ -1,23 +1,17 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "high_info_content(\"dns.question.name\") over tld", + "detector_description": "Detects high info content of DNS questions over a population of TLDs.", "function": "high_info_content", "field_name": "dns.question.name", "over_field_name": "dns.question.etld_plus_one", "custom_rules": [ { - "actions": [ - "skip_result" - ], + "actions": ["skip_result"], "conditions": [ { "applies_to": "actual", @@ -29,12 +23,7 @@ ] } ], - "influencers": [ - "destination.ip", - "host.name", - "dns.question.etld_plus_one" - ], - "model_prune_window": "30d" + "influencers": ["destination.ip", "host.name", "dns.question.etld_plus_one"] }, "allow_lazy_open": true, "analysis_limits": { @@ -45,12 +34,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "DNS Tunneling" + "security_app_display_name": "DNS Tunneling", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json index 36c8b3acd722e..049d4e3babd23 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_dns_question.json @@ -1,22 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual DNS activity that could indicate command-and-control activity.", - "groups": [ - "security", - "packetbeat", - "dns" - ], + "groups": ["security", "packetbeat", "dns"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"dns.question.name\"", + "detector_description": "Detects rare DNS question names.", "function": "rare", "by_field_name": "dns.question.name" } ], - "influencers": [ - "host.name" - ] + "influencers": ["host.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -27,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual DNS Activity" + "security_app_display_name": "Unusual DNS Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json index 3f3c137e8fd34..d8df5c4986b99 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_server_domain.json @@ -1,24 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP or TLS destination domain activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"server.domain\"", + "detector_description": "Detects rare server domains.", "function": "rare", "by_field_name": "server.domain" } ], - "influencers": [ - "host.name", - "destination.ip", - "source.ip" - ] + "influencers": ["host.name", "destination.ip", "source.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -29,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Network Destination Domain Name" + "security_app_display_name": "Unusual Network Destination Domain Name", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json index afa430bd835f2..055204dd1c376 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_urls.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual web browsing URL activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"url.full\"", + "detector_description": "Detects rare URLs.", "function": "rare", "by_field_name": "url.full" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web Request" + "security_app_display_name": "Unusual Web Request", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json index bb2d524b41c1f..c947e4f1d509b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_packetbeat/ml/packetbeat_rare_user_agent.json @@ -1,23 +1,16 @@ { "description": "Security: Packetbeat - Looks for unusual HTTP user agent activity that could indicate execution, persistence, command-and-control or data exfiltration activity.", - "groups": [ - "security", - "packetbeat", - "web" - ], + "groups": ["security", "packetbeat"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "rare by \"user_agent.original\"", + "detector_description": "Detects rare web user agents.", "function": "rare", "by_field_name": "user_agent.original" } ], - "influencers": [ - "host.name", - "destination.ip" - ] + "influencers": ["host.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { @@ -28,12 +21,8 @@ }, "custom_settings": { "created_by": "ml-module-security-packetbeat", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Web User Agent" + "security_app_display_name": "Unusual Web User Agent", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json index 6b7e5dcf56f1f..38fa9e2e4e904 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_rare_process_by_host_windows.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for processes that are unusual to a particular Windows host. Such unusual processes may indicate unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each host.name, detects rare process.name values.", + "detector_description": "Detects rare processes per host.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "host.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8001", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Process for a Windows Host" + "security_app_display_name": "Unusual Process for a Windows Host", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json index 04ee9912c15e3..2e04fa91be336 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_network_activity.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for unusual processes using the network which could indicate command-and-control, lateral movement, persistence, or data exfiltration activity.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare processes.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name", - "destination.ip" - ] + "influencers": ["host.name", "process.name", "user.name", "destination.ip"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "64mb", - "categorization_examples_limit": 4 + "model_memory_limit": "64mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8003", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Network Activity" + "security_app_display_name": "Unusual Windows Network Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json index d5c931b3c46e8..c9f0579309c6b 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_path_activity.json @@ -1,65 +1,29 @@ { "description": "Security: Windows - Looks for activity in unusual paths that may indicate execution of malware or persistence mechanisms. Windows payloads often execute from user profile paths.", - "groups": [ - "endpoint", - "network", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.working_directory values.", + "detector_description": "Detects rare working directories.", "function": "rare", "by_field_name": "process.working_directory", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8004", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Path Activity" + "security_app_display_name": "Unusual Windows Path Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json index 1474763cec7b9..08baa6587f9ff 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_all_hosts.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Looks for processes that are unusual to all Windows hosts. Such unusual processes may indicate execution of unauthorized software, malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.executable values.", + "detector_description": "Detects rare process executable values.", "function": "rare", - "by_field_name": "process.executable", + "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8002", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Process for a Windows Population" + "security_app_display_name": "Anomalous Process for a Windows Population", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json index 2966630fad878..1bf46c2d416a9 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_process_creation.json @@ -1,67 +1,30 @@ { "description": "Security: Windows - Looks for unusual process relationships which may indicate execution of malware or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "For each process.parent.name, detects rare process.name values.", + "detector_description": "Detects rare processes per parent process.", "function": "rare", "by_field_name": "process.name", "partition_field_name": "process.parent.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8005", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Anomalous Windows Process Creation" + "security_app_display_name": "Anomalous Windows Process Creation", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json index b01641b2ef3ad..5472ad77e1b70 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_script.json @@ -1,28 +1,17 @@ { "description": "Security: Windows - Looks for unusual powershell scripts that may indicate execution of malware, or persistence mechanisms.", - "groups": [ - "endpoint", - "event-log", - "process", - "windows", - "winlogbeat", - "powershell", - "security" - ], + "groups": ["windows", "powershell", "security"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects high information content in powershell.file.script_block_text values.", + "detector_description": "Detects high information content in powershell scripts.", "function": "high_info_content", - "field_name": "powershell.file.script_block_text" + "field_name": "powershell.file.script_block_text", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name", - "file.path" - ] + "influencers": ["host.name", "user.name", "file.path"] }, "allow_lazy_open": true, "analysis_limits": { @@ -32,24 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8006", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by user name", - "url_value": "siem#/ml-hosts/$host.name$?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.details,type:details)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "siem#/ml-hosts?_g=()&kqlQuery=(filterQuery:(expression:'user.name%20:%20%22$user.name$%22',kind:kuery),queryLocation:hosts.page,type:page)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Suspicious Powershell Script" + "security_app_display_name": "Suspicious Powershell Script", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json index 9716c8365e317..b2530538a9263 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_service.json @@ -1,27 +1,17 @@ { - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "description": "Security: Windows - Looks for rare and unusual Windows service names which may indicate execution of unauthorized services, malware, or persistence mechanisms.", "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare winlog.event_data.ServiceName values.", + "detector_description": "Detects rare service names.", "function": "rare", - "by_field_name": "winlog.event_data.ServiceName" + "by_field_name": "winlog.event_data.ServiceName", + "detector_index": 0 } ], - "influencers": [ - "host.name", - "winlog.event_data.ServiceName" - ] + "influencers": ["host.name", "winlog.event_data.ServiceName"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,20 +21,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8007", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Service" + "security_app_display_name": "Unusual Windows Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json index eda4b768b5308..659e58cfdba32 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_anomalous_user_name.json @@ -1,66 +1,29 @@ { "description": "Security: Windows - Rare and unusual users that are not normally active may indicate unauthorized changes or activity by an unauthorized user which may be credentialed access or lateral movement.", - "groups": [ - "endpoint", - "event-log", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "256mb", - "categorization_examples_limit": 4 + "model_memory_limit": "256mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8008", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Username" + "security_app_display_name": "Unusual Windows Username", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json index ab4fd311d6646..953a00a8fff52 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_process.json @@ -1,47 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "security", - "endpoint", - "process", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare process.name values.", + "detector_description": "Detects rare process names.", "function": "rare", "by_field_name": "process.name", "detector_index": 0 } ], - "influencers": [ - "process.name", - "host.name", - "user.name" - ] + "influencers": ["process.name", "host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8011", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows Process Calling the Metadata Service" + "security_app_display_name": "Unusual Windows Process Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json index fe8a634d49921..df55cb3d67709 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_metadata_user.json @@ -1,46 +1,29 @@ { "description": "Security: Windows - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", - "groups": [ - "endpoint", - "process", - "security", - "sysmon", - "windows", - "winlogbeat" - ], + "groups": ["security", "windows"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name", "detector_index": 0 } ], - "influencers": [ - "host.name", - "user.name" - ] + "influencers": ["host.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { - "model_memory_limit": "32mb", - "categorization_examples_limit": 4 + "model_memory_limit": "32mb" }, "data_description": { - "time_field": "@timestamp", - "time_format": "epoch_ms" + "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8012", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "security_app_display_name": "Unusual Windows User Calling the Metadata Service" + "security_app_display_name": "Unusual Windows User Calling the Metadata Service", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json index b95aa1144f440..87d9d4b172f63 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_runas_event.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual user context switches can be due to privilege escalation.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8009", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows User Privilege Elevation Activity" + "security_app_display_name": "Unusual Windows User Privilege Elevation Activity", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json index a6ec19401190f..e118f761453be 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/security_windows/ml/v3_windows_rare_user_type10_remote_login.json @@ -1,27 +1,16 @@ { "description": "Security: Windows - Unusual RDP (remote desktop protocol) user logins can indicate account takeover or credentialed access.", - "groups": [ - "endpoint", - "event-log", - "security", - "windows", - "winlogbeat", - "authentication" - ], + "groups": ["security", "windows", "authentication"], "analysis_config": { "bucket_span": "15m", "detectors": [ { - "detector_description": "Detects rare user.name values.", + "detector_description": "Detects rare usernames.", "function": "rare", "by_field_name": "user.name" } ], - "influencers": [ - "host.name", - "process.name", - "user.name" - ] + "influencers": ["host.name", "process.name", "user.name"] }, "allow_lazy_open": true, "analysis_limits": { @@ -31,32 +20,9 @@ "time_field": "@timestamp" }, "custom_settings": { - "job_tags": { - "euid": "8013", - "maturity": "release", - "author": "@randomuserid/Elastic", - "version": "3", - "updated_date": "5/16/2022" - }, "created_by": "ml-module-security-windows-v3", - "custom_urls": [ - { - "url_name": "Host Details by process name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Host Details by user name", - "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by process name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - }, - { - "url_name": "Hosts Overview by user name", - "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" - } - ], - "security_app_display_name": "Unusual Windows Remote User" + "security_app_display_name": "Unusual Windows Remote User", + "managed": true, + "job_revision": 4 } } diff --git a/x-pack/plugins/ml/tsconfig.json b/x-pack/plugins/ml/tsconfig.json index 6179571a28515..8288c9998fc7d 100644 --- a/x-pack/plugins/ml/tsconfig.json +++ b/x-pack/plugins/ml/tsconfig.json @@ -30,7 +30,6 @@ "@kbn/charts-plugin", "@kbn/cloud-plugin", "@kbn/config-schema", - "@kbn/core-http-browser", "@kbn/dashboard-plugin", "@kbn/data-plugin", "@kbn/data-views-plugin", @@ -90,5 +89,6 @@ "@kbn/unified-search-plugin", "@kbn/usage-collection-plugin", "@kbn/utility-types", + "@kbn/ml-error-utils", ], } diff --git a/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx b/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx new file mode 100644 index 0000000000000..f72e4b008f390 --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/feedback_button/feedback_button.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +const SLO_FEEDBACK_LINK = 'https://ela.st/slo-feedback'; + +export function FeedbackButton() { + return ( + + {i18n.translate('xpack.observability.slo.feedbackButtonLabel', { + defaultMessage: 'Tell us what you think!', + })} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx index 7fc212dbd4275..dbbacc6b1be91 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/components/wide_chart.tsx @@ -15,10 +15,11 @@ import { ScaleType, Settings, } from '@elastic/charts'; -import React from 'react'; +import React, { useRef } from 'react'; import { EuiIcon, EuiLoadingChart, useEuiTheme } from '@elastic/eui'; import numeral from '@elastic/numeral'; import moment from 'moment'; +import { useActiveCursor } from '@kbn/charts-plugin/public'; import { ChartData } from '../../../typings'; import { useKibana } from '../../../utils/kibana_react'; @@ -45,18 +46,29 @@ export function WideChart({ chart, data, id, isLoading, state }: Props) { const color = state === 'error' ? euiTheme.colors.danger : euiTheme.colors.success; const ChartComponent = chart === 'area' ? AreaSeries : LineSeries; + const chartRef = useRef(null); + const handleCursorUpdate = useActiveCursor(charts.activeCursor, chartRef, { + isDateHistogram: true, + }); + if (isLoading) { return ; } return ( - + } + onPointerUpdate={handleCursorUpdate} + externalPointerEvents={{ + tooltip: { visible: true }, + }} + pointerUpdateDebounce={0} + pointerUpdateTrigger={'x'} /> { useKibanaMock.mockReturnValue({ services: { application: { navigateToUrl: mockNavigate }, - charts: chartPluginMock.createSetupContract(), + charts: chartPluginMock.createStartContract(), http: { basePath: { prepend: mockBasePathPrepend, diff --git a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx index bec6e6608abcc..48401ca29f94e 100644 --- a/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx +++ b/x-pack/plugins/observability/public/pages/slo_details/slo_details.tsx @@ -26,6 +26,7 @@ import { paths } from '../../config/paths'; import type { SloDetailsPathParams } from './types'; import type { ObservabilityAppServices } from '../../application/types'; import { AutoRefreshButton } from '../slos/components/auto_refresh_button'; +import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button'; export function SloDetailsPage() { const { @@ -69,6 +70,7 @@ export function SloDetailsPage() { isAutoRefreshing={isAutoRefreshing} onClick={handleToggleAutoRefresh} />, + , ], bottomBorder: false, }} diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx index 57e27810815d6..ede2d15d3f23d 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.tsx @@ -16,6 +16,7 @@ import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; import { useFetchSloDetails } from '../../hooks/slo/use_fetch_slo_details'; import { useLicense } from '../../hooks/use_license'; import { SloEditForm } from './components/slo_edit_form'; +import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button'; export function SloEditPage() { const { @@ -58,7 +59,7 @@ export function SloEditPage() { : i18n.translate('xpack.observability.sloCreatePageTitle', { defaultMessage: 'Create new SLO', }), - rightSideItems: [], + rightSideItems: [], bottomBorder: false, }} data-test-subj="slosEditPage" diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx index c60617152cb00..2b80e8852bdae 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_rules_badge.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; -import { EuiBadge, EuiIcon, EuiToolTip } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { Rule } from '@kbn/triggers-actions-ui-plugin/public'; + import { SloRule } from '../../../../hooks/slo/use_fetch_rules_for_slo'; export interface Props { @@ -26,16 +27,9 @@ export function SloRulesBadge({ rules, onClick }: Props) { })} display="block" > - - - + + + ); } diff --git a/x-pack/plugins/observability/public/pages/slos/slos.tsx b/x-pack/plugins/observability/public/pages/slos/slos.tsx index e224158ef9c2a..b81998d8452de 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.tsx @@ -21,6 +21,7 @@ import { AutoRefreshButton } from './components/auto_refresh_button'; import { paths } from '../../config/paths'; import type { ObservabilityAppServices } from '../../application/types'; import { HeaderTitle } from './components/header_title'; +import { FeedbackButton } from '../../components/slo/feedback_button/feedback_button'; export function SlosPage() { const { @@ -69,7 +70,7 @@ export function SlosPage() { rightSideItems: [ , + , ], bottomBorder: false, }} diff --git a/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx index 5a562958cb299..a0e5a1f0e037b 100644 --- a/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/plugins/observability/public/utils/kibana_react.storybook_decorator.tsx @@ -65,6 +65,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { useChartsBaseTheme: () => {}, useChartsTheme: () => {}, }, + activeCursor: () => {}, }, data: {}, dataViews: { diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts index 1e55fbe047ddc..3bf83993df1e8 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.test.ts @@ -2098,6 +2098,7 @@ describe('createGetSummarizedAlertsFn', () => { query: { kql: 'kibana.alert.rule.name:test', dsl: '{"bool":{"minimum_should_match":1,"should":[{"match":{"kibana.alert.rule.name":"test"}}]}}', + filters: [], }, timeframe: { days: [1, 2, 3, 4, 5], @@ -2147,11 +2148,12 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', days: [1, 2, 3, 4, 5], timezone: 'UTC', }, source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', }, }, }, @@ -2159,17 +2161,18 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', end: '17:00', start: '08:00', timezone: 'UTC', }, source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { @@ -2231,11 +2234,12 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', days: [1, 2, 3, 4, 5], timezone: 'UTC', }, source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', }, }, }, @@ -2243,17 +2247,18 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', end: '17:00', start: '08:00', timezone: 'UTC', }, source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { @@ -2315,11 +2320,12 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', days: [1, 2, 3, 4, 5], timezone: 'UTC', }, source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', }, }, }, @@ -2327,17 +2333,18 @@ describe('createGetSummarizedAlertsFn', () => { script: { script: { params: { + datetimeField: '@timestamp', end: '17:00', start: '08:00', timezone: 'UTC', }, source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { diff --git a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts index 4a7a727276ad4..d4fe127b240ee 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_get_summarized_alerts_fn.ts @@ -573,10 +573,11 @@ const generateAlertsFilterDSL = (alertsFilter: AlertsFilter): QueryDslQueryConta script: { script: { source: - "params.days.contains(doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())", + 'params.days.contains(doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)).dayOfWeek.getValue())', params: { days: alertsFilter.timeframe.days, timezone: alertsFilter.timeframe.timezone, + datetimeField: TIMESTAMP, }, }, }, @@ -585,12 +586,12 @@ const generateAlertsFilterDSL = (alertsFilter: AlertsFilter): QueryDslQueryConta script: { script: { source: ` - def alertsDateTime = doc['kibana.alert.start'].value.withZoneSameInstant(ZoneId.of(params.timezone)); + def alertsDateTime = doc[params.datetimeField].value.withZoneSameInstant(ZoneId.of(params.timezone)); def alertsTime = LocalTime.of(alertsDateTime.getHour(), alertsDateTime.getMinute()); def start = LocalTime.parse(params.start); def end = LocalTime.parse(params.end); - if (end.isBefore(start)){ // overnight + if (end.isBefore(start) || end.equals(start)){ // overnight def dayEnd = LocalTime.parse("23:59:59"); def dayStart = LocalTime.parse("00:00:00"); if ((alertsTime.isAfter(start) && alertsTime.isBefore(dayEnd)) || (alertsTime.isAfter(dayStart) && alertsTime.isBefore(end))) { @@ -610,6 +611,7 @@ const generateAlertsFilterDSL = (alertsFilter: AlertsFilter): QueryDslQueryConta start: alertsFilter.timeframe.hours.start, end: alertsFilter.timeframe.hours.end, timezone: alertsFilter.timeframe.timezone, + datetimeField: TIMESTAMP, }, }, }, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index aba27d9b617d5..2551bc34f7fa4 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; + /** * as const * @@ -377,9 +379,18 @@ export const ML_GROUP_ID = 'security' as const; export const LEGACY_ML_GROUP_ID = 'siem' as const; export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID] as const; +/** + * Rule Actions + */ export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions' as const; export const NOTIFICATION_THROTTLE_RULE = 'rule' as const; +export const NOTIFICATION_DEFAULT_FREQUENCY = { + notifyWhen: RuleNotifyWhen.ACTIVE, + throttle: null, + summary: true, +}; + export const showAllOthersBucket: string[] = [ 'destination.ip', 'event.action', diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts index 3cee4c3dbe384..712312c50140d 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.test.ts @@ -133,7 +133,10 @@ describe('Perform bulk action request schema', () => { const payload: PerformBulkActionRequestBody = { query: 'name: test', action: BulkActionType.duplicate, - [BulkActionType.duplicate]: { include_exceptions: false }, + [BulkActionType.duplicate]: { + include_exceptions: false, + include_expired_exceptions: false, + }, }; const message = retrieveValidationMessage(payload); expect(getPaths(left(message.errors))).toEqual([]); @@ -512,28 +515,6 @@ describe('Perform bulk action request schema', () => { expect(message.schema).toEqual({}); }); - test('invalid request: missing throttle in payload', () => { - const payload = { - query: 'name: test', - action: BulkActionType.edit, - [BulkActionType.edit]: [ - { - type: BulkActionEditType.add_rule_actions, - value: { - actions: [], - }, - }, - ], - }; - - const message = retrieveValidationMessage(payload); - - expect(getPaths(left(message.errors))).toEqual( - expect.arrayContaining(['Invalid value "undefined" supplied to "edit,value,throttle"']) - ); - expect(message.schema).toEqual({}); - }); - test('invalid request: missing actions in payload', () => { const payload = { query: 'name: test', diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts index b0860c55ebd5a..e0a392885bad9 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema.ts @@ -9,6 +9,7 @@ import * as t from 'io-ts'; import { NonEmptyArray, TimeDuration } from '@kbn/securitysolution-io-ts-types'; import { + RuleActionFrequency, RuleActionGroup, RuleActionId, RuleActionParams, @@ -96,11 +97,14 @@ const BulkActionEditPayloadTimeline = t.type({ */ type NormalizedRuleAction = t.TypeOf; const NormalizedRuleAction = t.exact( - t.type({ - group: RuleActionGroup, - id: RuleActionId, - params: RuleActionParams, - }) + t.intersection([ + t.type({ + group: RuleActionGroup, + id: RuleActionId, + params: RuleActionParams, + }), + t.partial({ frequency: RuleActionFrequency }), + ]) ); export type BulkActionEditPayloadRuleActions = t.TypeOf; @@ -109,10 +113,12 @@ export const BulkActionEditPayloadRuleActions = t.type({ t.literal(BulkActionEditType.add_rule_actions), t.literal(BulkActionEditType.set_rule_actions), ]), - value: t.type({ - throttle: ThrottleForBulkActions, - actions: t.array(NormalizedRuleAction), - }), + value: t.intersection([ + t.partial({ throttle: ThrottleForBulkActions }), + t.type({ + actions: t.array(NormalizedRuleAction), + }), + ]), }); type BulkActionEditPayloadSchedule = t.TypeOf; @@ -136,6 +142,7 @@ export const BulkActionEditPayload = t.union([ const bulkActionDuplicatePayload = t.exact( t.type({ include_exceptions: t.boolean, + include_expired_exceptions: t.boolean, }) ); diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_management/constants.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_management/constants.ts index 710c0b55a86f9..ba290d30b57a3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_management/constants.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_management/constants.ts @@ -7,5 +7,6 @@ export enum DuplicateOptions { withExceptions = 'withExceptions', + withExceptionsExcludeExpiredExceptions = 'withExceptionsExcludeExpiredExceptions', withoutExceptions = 'withoutExceptions', } diff --git a/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts index e0b427cdcefbc..ff4bb72a5eb65 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/rule_schema/model/rule_schemas.ts @@ -119,6 +119,8 @@ export const baseSchema = buildRuleSchemas({ output_index: AlertsIndex, namespace: AlertsIndexNamespace, meta: RuleMetadata, + // Throttle + throttle: RuleActionThrottle, }, defaultable: { // Main attributes @@ -134,7 +136,6 @@ export const baseSchema = buildRuleSchemas({ to: RuleIntervalTo, // Rule actions actions: RuleActionArray, - throttle: RuleActionThrottle, // Rule exceptions exceptions_list: ExceptionListArray, // Misc attributes diff --git a/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts index 1b74bcd320aad..3808837dc0df2 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/transform_actions.ts @@ -16,6 +16,7 @@ export const transformRuleToAlertAction = ({ action_type_id: actionTypeId, params, uuid, + frequency, alerts_filter: alertsFilter, }: RuleAlertAction): RuleAction => ({ group, @@ -24,6 +25,7 @@ export const transformRuleToAlertAction = ({ actionTypeId, ...(alertsFilter && { alertsFilter }), ...(uuid && { uuid }), + ...(frequency && { frequency }), }); export const transformAlertToRuleAction = ({ @@ -32,6 +34,7 @@ export const transformAlertToRuleAction = ({ actionTypeId, params, uuid, + frequency, alertsFilter, }: RuleAction): RuleAlertAction => ({ group, @@ -40,6 +43,7 @@ export const transformAlertToRuleAction = ({ action_type_id: actionTypeId, ...(alertsFilter && { alerts_filter: alertsFilter }), ...(uuid && { uuid }), + ...(frequency && { frequency }), }); export const transformRuleToAlertResponseAction = ({ diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts index 855dd3a3fe439..fdf75da0a134e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_action_generator.ts @@ -155,38 +155,37 @@ export class EndpointActionGenerator extends BaseDataGenerator { TOutputType extends object = object, TParameters extends EndpointActionDataParameterTypes = EndpointActionDataParameterTypes >( - overrides: Partial> = {} + overrides: DeepPartial> = {} ): ActionDetails { - const details: ActionDetails = merge( - { - agents: ['agent-a'], - command: 'isolate', - completedAt: '2022-04-30T16:08:47.449Z', - hosts: { 'agent-a': { name: 'Host-agent-a' } }, - id: '123', - isCompleted: true, - isExpired: false, - wasSuccessful: true, - errors: undefined, - startedAt: '2022-04-27T16:08:47.449Z', - status: 'successful', - comment: 'thisisacomment', - createdBy: 'auserid', - parameters: undefined, - outputs: {}, - agentState: { - 'agent-a': { - errors: undefined, - isCompleted: true, - completedAt: '2022-04-30T16:08:47.449Z', - wasSuccessful: true, - }, + const details: ActionDetails = { + agents: ['agent-a'], + command: 'isolate', + completedAt: '2022-04-30T16:08:47.449Z', + hosts: { 'agent-a': { name: 'Host-agent-a' } }, + id: '123', + isCompleted: true, + isExpired: false, + wasSuccessful: true, + errors: undefined, + startedAt: '2022-04-27T16:08:47.449Z', + status: 'successful', + comment: 'thisisacomment', + createdBy: 'auserid', + parameters: undefined, + outputs: {}, + agentState: { + 'agent-a': { + errors: undefined, + isCompleted: true, + completedAt: '2022-04-30T16:08:47.449Z', + wasSuccessful: true, }, }, - overrides - ); + }; - if (details.command === 'get-file') { + const command = overrides.command ?? details.command; + + if (command === 'get-file') { if (!details.parameters) { ( details as ActionDetails< @@ -213,7 +212,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { } } - if (details.command === 'execute') { + if (command === 'execute') { if (!details.parameters) { ( details as ActionDetails< @@ -233,14 +232,17 @@ export class EndpointActionGenerator extends BaseDataGenerator { [details.agents[0]]: this.generateExecuteActionResponseOutput({ content: { output_file_id: getFileDownloadId(details, details.agents[0]), - ...overrides.outputs?.[details.agents[0]].content, + ...(overrides.outputs?.[details.agents[0]]?.content ?? {}), }, }), }; } } - return details as unknown as ActionDetails; + return merge(details, overrides as ActionDetails) as unknown as ActionDetails< + TOutputType, + TParameters + >; } randomGetFileFailureCode(): string { @@ -310,6 +312,7 @@ export class EndpointActionGenerator extends BaseDataGenerator { { type: 'json', content: { + code: 'ra_execute_success_done', stdout: this.randomChoice([ this.randomString(1280), this.randomString(3580), diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts index e5a44c46f5afc..bc429feb208d7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_metadata_generator.ts @@ -20,6 +20,7 @@ export interface GetCustomEndpointMetadataGeneratorOptions { version: string; /** OS type for the generated endpoint hosts */ os: 'macOS' | 'windows' | 'linux'; + isolation: boolean; } /** @@ -33,6 +34,7 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { static custom({ version, os, + isolation, }: Partial = {}): typeof EndpointMetadataGenerator { return class extends EndpointMetadataGenerator { generate(overrides: DeepPartial = {}): HostMetadataInterface { @@ -54,6 +56,9 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { set(overrides, 'host.os', EndpointMetadataGenerator.windowsOSFields); } } + if (isolation !== undefined) { + set(overrides, 'Endpoint.state.isolation', isolation); + } return super.generate(overrides); } @@ -104,10 +109,10 @@ export class EndpointMetadataGenerator extends BaseDataGenerator { /** Generate an Endpoint host metadata document */ generate(overrides: DeepPartial = {}): HostMetadataInterface { const ts = overrides['@timestamp'] ?? new Date().getTime(); - const hostName = this.randomHostname(); + const hostName = overrides?.host?.hostname ?? this.randomHostname(); const agentVersion = overrides?.agent?.version ?? this.randomVersion(); const agentId = this.seededUUIDv4(); - const isIsolated = this.randomBoolean(0.3); + const isIsolated = overrides?.Endpoint?.state?.isolation ?? this.randomBoolean(0.3); const capabilities: EndpointCapabilities[] = ['isolation']; // v8.4 introduced additional endpoint capabilities diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts index 5578c179ba1f5..8396f86a45e97 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/endpoint_rule_alert_generator.ts @@ -37,6 +37,8 @@ export class EndpointRuleAlertGenerator extends BaseDataGenerator { const endpointMetadataGenerator = new EndpointMetadataGenerator(); const endpointMetadata = endpointMetadataGenerator.generate({ agent: { version: kibanaPackageJson.version }, + host: { hostname: overrides?.host?.hostname }, + Endpoint: { state: { isolation: overrides?.Endpoint?.state?.isolation } }, }); const now = overrides['@timestamp'] ?? new Date().toISOString(); const endpointAgentId = overrides?.agent?.id ?? this.seededUUIDv4(); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts index f9f96e650c056..684694bdb5c9a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_hosts.ts @@ -75,6 +75,7 @@ export interface IndexedHostsResponse * @param policyResponseIndex * @param enrollFleet * @param generator + * @param disableEndpointActionsForHost */ export async function indexEndpointHostDocs({ numDocs, @@ -86,6 +87,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex, enrollFleet, generator, + withResponseActions = true, }: { numDocs: number; client: Client; @@ -96,6 +98,7 @@ export async function indexEndpointHostDocs({ policyResponseIndex: string; enrollFleet: boolean; generator: EndpointDocGenerator; + withResponseActions?: boolean; }): Promise { const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents const timestamp = new Date().getTime(); @@ -190,13 +193,15 @@ export async function indexEndpointHostDocs({ }, }; - // Create some fleet endpoint actions and .logs-endpoint actions for this Host - const actionsResponse = await indexEndpointAndFleetActionsForHost( - client, - hostMetadata, - undefined - ); - mergeAndAppendArrays(response, actionsResponse); + if (withResponseActions) { + // Create some fleet endpoint actions and .logs-endpoint actions for this Host + const actionsResponse = await indexEndpointAndFleetActionsForHost( + client, + hostMetadata, + undefined + ); + mergeAndAppendArrays(response, actionsResponse); + } } await client diff --git a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts index 74e9d82a714e9..1c5883c052135 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_loaders/index_endpoint_rule_alerts.ts @@ -22,6 +22,8 @@ import { EndpointRuleAlertGenerator } from '../data_generators/endpoint_rule_ale export interface IndexEndpointRuleAlertsOptions { esClient: Client; endpointAgentId: string; + endpointHostname?: string; + endpointIsolated?: boolean; count?: number; log?: ToolingLog; } @@ -40,12 +42,16 @@ export interface DeletedIndexedEndpointRuleAlerts { * written them to for a given endpoint * @param esClient * @param endpointAgentId + * @param endpointHostname + * @param endpointIsolated * @param count * @param log */ export const indexEndpointRuleAlerts = async ({ esClient, endpointAgentId, + endpointHostname, + endpointIsolated, count = 1, log = new ToolingLog(), }: IndexEndpointRuleAlertsOptions): Promise => { @@ -57,7 +63,11 @@ export const indexEndpointRuleAlerts = async ({ const indexedAlerts: estypes.IndexResponse[] = []; for (let n = 0; n < count; n++) { - const alert = alertsGenerator.generate({ agent: { id: endpointAgentId } }); + const alert = alertsGenerator.generate({ + agent: { id: endpointAgentId }, + host: { hostname: endpointHostname }, + ...(endpointIsolated ? { Endpoint: { state: { isolation: endpointIsolated } } } : {}), + }); const indexedAlert = await esClient.index({ index: `${DEFAULT_ALERTS_INDEX}-default`, refresh: 'wait_for', diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index d867c25ececf7..76ee903eb6889 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -410,7 +410,9 @@ export class EndpointDocGenerator extends BaseDataGenerator { private createHostData(): CommonHostInfo { const { agent, elastic, host, Endpoint } = this.metadataGenerator.generate({ - Endpoint: { policy: { applied: this.randomChoice(APPLIED_POLICIES) } }, + Endpoint: { + policy: { applied: this.randomChoice(APPLIED_POLICIES) }, + }, }); return { agent, elastic, host, Endpoint }; diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 77b3135c12353..db5039e5a72f0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -48,6 +48,7 @@ export type IndexedHostsAndAlertsResponse = IndexedHostsResponse; * @param fleet * @param options * @param DocGenerator + * @param withResponseActions */ export async function indexHostsAndAlerts( client: Client, @@ -62,7 +63,8 @@ export async function indexHostsAndAlerts( alertsPerHost: number, fleet: boolean, options: TreeOptions = {}, - DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator + DocGenerator: typeof EndpointDocGenerator = EndpointDocGenerator, + withResponseActions = true ): Promise { const random = seedrandom(seed); const epmEndpointPackage = await getEndpointPackageInfo(kbnClient); @@ -114,6 +116,7 @@ export async function indexHostsAndAlerts( policyResponseIndex, enrollFleet: fleet, generator, + withResponseActions, }); mergeAndAppendArrays(response, indexedHosts); diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index d9082f8aa1d8c..f8f5da28943b8 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -65,6 +65,7 @@ export interface ResponseActionGetFileOutputContent { } export interface ResponseActionExecuteOutputContent { + code: string; /* The truncated 'tail' output of the command */ stdout: string; /* The truncated 'tail' of any errors generated by the command */ diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts index a5442b786040f..a15669a620d49 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel.cy.ts @@ -100,9 +100,7 @@ describe.skip('Alert details expandable flyout left panel', { testIsolation: fal it('should display content when switching buttons', () => { openVisualizeTab(); openSessionView(); - cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT) - .should('be.visible') - .and('have.text', 'Session view'); + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT).should('be.visible'); openGraphAnalyzer(); cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT).should('be.visible'); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts new file mode 100644 index 0000000000000..1d18b33350fd4 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_left_panel_session_view_tab.cy.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA } from '../../../screens/document_expandable_flyout'; +import { + expandFirstAlertExpandableFlyout, + expandDocumentDetailsExpandableFlyoutLeftSection, +} from '../../../tasks/document_expandable_flyout'; +import { cleanKibana } from '../../../tasks/common'; +import { login, visit } from '../../../tasks/login'; +import { createRule } from '../../../tasks/api_calls/rules'; +import { getNewRule } from '../../../objects/rule'; +import { ALERTS_URL } from '../../../urls/navigation'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; + +// Skipping these for now as the feature is protected behind a feature flag set to false by default +// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50 +describe.skip( + 'Alert details expandable flyout left panel session view', + { testIsolation: false }, + () => { + before(() => { + cleanKibana(); + login(); + createRule(getNewRule()); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + expandDocumentDetailsExpandableFlyoutLeftSection(); + }); + + it('should display session view no data message', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA) + .should('be.visible') + .and('contain.text', 'No data to render') + .and('contain.text', 'No process events found for this query'); + }); + + it('should display session view component', () => {}); + } +); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts index c98c84b800079..57ea1a2d4efdb 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_right_panel_overview_tab.cy.ts @@ -29,6 +29,10 @@ import { DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_VIEW_ALL_ENTITIES_BUTTON, DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT, DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_ANALYZER_TREE, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES, + DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON, } from '../../../screens/document_expandable_flyout'; import { expandFirstAlertExpandableFlyout, @@ -180,6 +184,38 @@ describe.skip( .click(); cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); }); + + // TODO work on getting proper IoC data to make the threat intelligence section work here + it.skip('should display threat intelligence section', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER) + .scrollIntoView() + .should('be.visible') + .and('have.text', 'Threat Intelligence'); + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT) + .should('be.visible') + .within(() => { + // threat match detected + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(0) + .should('be.visible') + .and('have.text', '1 threat match detected'); // TODO + + // field with threat enrichement + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES) + .eq(1) + .should('be.visible') + .and('have.text', '1 field enriched with threat intelligence'); // TODO + }); + }); + + // TODO work on getting proper IoC data to make the threat intelligence section work here + // and improve when we can navigate Threat Intelligence to sub tab directly + it.skip('should navigate to left panel, entities tab when view all fields of threat intelligence is clicked', () => { + cy.get(DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON) + .should('be.visible') + .click(); + cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_ENTITIES_CONTENT).should('be.visible'); + }); }); describe('visualizations section', () => { diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_url_sync.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_url_sync.cy.ts new file mode 100644 index 0000000000000..b7580642ba564 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_alerts/expandable_flyout/alert_details_url_sync.cy.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getNewRule } from '../../../objects/rule'; +import { cleanKibana } from '../../../tasks/common'; +import { waitForAlertsToPopulate } from '../../../tasks/create_new_rule'; +import { expandFirstAlertExpandableFlyout } from '../../../tasks/document_expandable_flyout'; +import { login, visit } from '../../../tasks/login'; +import { createRule } from '../../../tasks/api_calls/rules'; +import { ALERTS_URL } from '../../../urls/navigation'; +import { + DOCUMENT_DETAILS_FLYOUT_CLOSE_BUTTON, + DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE, +} from '../../../screens/document_expandable_flyout'; + +// Skipping these for now as the feature is protected behind a feature flag set to false by default +// To run the tests locally, add 'securityFlyoutEnabled' in the Cypress config.ts here https://github.com/elastic/kibana/blob/main/x-pack/test/security_solution_cypress/config.ts#L50 +describe.skip('Expandable flyout state sync', { testIsolation: false }, () => { + const rule = getNewRule(); + + before(() => { + cleanKibana(); + login(); + createRule(rule); + visit(ALERTS_URL); + waitForAlertsToPopulate(); + expandFirstAlertExpandableFlyout(); + }); + + it('should serialize its state to url', () => { + cy.url().should('include', 'eventFlyout'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + }); + + it('should reopen the flyout after browser refresh', () => { + cy.reload(); + + cy.url().should('include', 'eventFlyout'); + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + }); + + it('should clear the url state when flyout is closed', () => { + cy.reload(); + + cy.get(DOCUMENT_DETAILS_FLYOUT_HEADER_TITLE).should('be.visible').and('have.text', rule.name); + + cy.get(DOCUMENT_DETAILS_FLYOUT_CLOSE_BUTTON).click(); + + cy.url().should('not.include', 'eventFlyout'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_duplicate_rules.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_duplicate_rules.cy.ts new file mode 100644 index 0000000000000..ca25b9955bf97 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_duplicate_rules.cy.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + waitForRulesTableToBeLoaded, + goToTheRuleDetailsOf, + selectNumberOfRules, + duplicateSelectedRulesWithoutExceptions, + expectManagementTableRules, + duplicateSelectedRulesWithExceptions, + duplicateSelectedRulesWithNonExpiredExceptions, +} from '../../tasks/alerts_detection_rules'; + +import { goToExceptionsTab, viewExpiredExceptionItems } from '../../tasks/rule_details'; +import { login, visitWithoutDateRange } from '../../tasks/login'; + +import { SECURITY_DETECTIONS_RULES_URL } from '../../urls/navigation'; +import { createRule } from '../../tasks/api_calls/rules'; +import { cleanKibana, resetRulesTableState, deleteAlertsAndRules } from '../../tasks/common'; + +import { getNewRule } from '../../objects/rule'; + +import { esArchiverResetKibana } from '../../tasks/es_archiver'; + +import { createRuleExceptionItem } from '../../tasks/api_calls/exceptions'; +import { EXCEPTION_CARD_ITEM_NAME } from '../../screens/exceptions'; +import { + assertExceptionItemsExists, + assertNumberOfExceptionItemsExists, +} from '../../tasks/exceptions'; + +const RULE_NAME = 'Custom rule for bulk actions'; + +const prePopulatedIndexPatterns = ['index-1-*', 'index-2-*']; +const prePopulatedTags = ['test-default-tag-1', 'test-default-tag-2']; + +const defaultRuleData = { + index: prePopulatedIndexPatterns, + tags: prePopulatedTags, +}; + +const expiredDate = new Date(Date.now() - 1000000).toISOString(); +const futureDate = new Date(Date.now() + 1000000).toISOString(); + +const EXPIRED_EXCEPTION_ITEM_NAME = 'Sample exception item'; + +const NON_EXPIRED_EXCEPTION_ITEM_NAME = 'Sample exception item with future expiration'; + +describe('Detection rules, bulk duplicate', () => { + before(() => { + cleanKibana(); + login(); + }); + beforeEach(() => { + // Make sure persisted rules table state is cleared + resetRulesTableState(); + deleteAlertsAndRules(); + esArchiverResetKibana(); + createRule(getNewRule({ name: RULE_NAME, ...defaultRuleData, rule_id: '1' })).then( + (response) => { + createRuleExceptionItem(response.body.id, [ + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'user.name', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: EXPIRED_EXCEPTION_ITEM_NAME, + type: 'simple', + expire_time: expiredDate, + }, + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'user.name', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: NON_EXPIRED_EXCEPTION_ITEM_NAME, + type: 'simple', + expire_time: futureDate, + }, + ]); + } + ); + + visitWithoutDateRange(SECURITY_DETECTIONS_RULES_URL); + + waitForRulesTableToBeLoaded(); + }); + + it('Duplicates rules', () => { + selectNumberOfRules(1); + duplicateSelectedRulesWithoutExceptions(); + expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); + }); + + describe('With exceptions', () => { + it('Duplicates rules with expired exceptions', () => { + selectNumberOfRules(1); + duplicateSelectedRulesWithExceptions(); + expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); + goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`); + goToExceptionsTab(); + assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]); + viewExpiredExceptionItems(); + assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [EXPIRED_EXCEPTION_ITEM_NAME]); + }); + + it('Duplicates rules with exceptions, excluding expired exceptions', () => { + selectNumberOfRules(1); + duplicateSelectedRulesWithNonExpiredExceptions(); + expectManagementTableRules([`${RULE_NAME} [Duplicate]`]); + goToTheRuleDetailsOf(`${RULE_NAME} [Duplicate]`); + goToExceptionsTab(); + assertExceptionItemsExists(EXCEPTION_CARD_ITEM_NAME, [NON_EXPIRED_EXCEPTION_ITEM_NAME]); + viewExpiredExceptionItems(); + assertNumberOfExceptionItemsExists(0); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts index fceecae2b1d5b..624dec7f1c955 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/bulk_edit_rules_actions.cy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; import { ROLES } from '../../../common/test'; import { @@ -15,11 +16,18 @@ import { import { actionFormSelector } from '../../screens/common/rule_actions'; import { cleanKibana, deleteAlertsAndRules, deleteConnectors } from '../../tasks/common'; +import type { RuleActionCustomFrequency } from '../../tasks/common/rule_actions'; import { addSlackRuleAction, assertSlackRuleAction, addEmailConnectorAndRuleAction, assertEmailRuleAction, + assertSelectedCustomFrequencyOption, + assertSelectedPerRuleRunFrequencyOption, + assertSelectedSummaryOfAlertsOption, + pickCustomFrequencyOption, + pickPerRuleRunFrequencyOption, + pickSummaryOfAlertsOption, } from '../../tasks/common/rule_actions'; import { waitForRulesTableToBeLoaded, @@ -32,10 +40,8 @@ import { submitBulkEditForm, checkOverwriteRuleActionsCheckbox, openBulkEditRuleActionsForm, - pickActionFrequency, openBulkActionsMenu, } from '../../tasks/rules_bulk_edit'; -import { assertSelectedActionFrequency } from '../../tasks/edit_rule'; import { login, visitWithoutDateRange } from '../../tasks/login'; import { esArchiverResetKibana } from '../../tasks/es_archiver'; @@ -75,7 +81,7 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { esArchiverResetKibana(); createSlackConnector().then(({ body }) => { - const actions = [ + const actions: RuleActionArray = [ { id: body.id, action_type_id: '.slack', @@ -83,6 +89,11 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { params: { message: expectedExistingSlackMessage, }, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, }, ]; @@ -120,7 +131,10 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { }); it('Add a rule action to rules (existing connector)', () => { - const expectedActionFrequency = 'Daily'; + const expectedActionFrequency: RuleActionCustomFrequency = { + throttle: 1, + throttleUnit: 'd', + }; loadPrebuiltDetectionRulesFromHeaderBtn(); @@ -131,8 +145,9 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // ensure rule actions info callout displayed on the form cy.get(RULES_BULK_EDIT_ACTIONS_INFO).should('be.visible'); - pickActionFrequency(expectedActionFrequency); addSlackRuleAction(expectedSlackMessage); + pickSummaryOfAlertsOption(); + pickCustomFrequencyOption(expectedActionFrequency); submitBulkEditForm(); waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfRulesToBeEdited }); @@ -140,7 +155,8 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedCustomFrequencyOption(expectedActionFrequency, 1); assertSlackRuleAction(expectedExistingSlackMessage, 0); assertSlackRuleAction(expectedSlackMessage, 1); // ensure there is no third action @@ -148,16 +164,15 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { }); it('Overwrite rule actions in rules', () => { - const expectedActionFrequency = 'On each rule execution'; - loadPrebuiltDetectionRulesFromHeaderBtn(); // select both custom and prebuilt rules selectNumberOfRules(expectedNumberOfRulesToBeEdited); openBulkEditRuleActionsForm(); - pickActionFrequency(expectedActionFrequency); addSlackRuleAction(expectedSlackMessage); + pickSummaryOfAlertsOption(); + pickPerRuleRunFrequencyOption(); // check overwrite box, ensure warning is displayed checkOverwriteRuleActionsCheckbox(); @@ -171,22 +186,27 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedPerRuleRunFrequencyOption(); assertSlackRuleAction(expectedSlackMessage); // ensure existing action was overwritten cy.get(actionFormSelector(1)).should('not.exist'); }); it('Add a rule action to rules (new connector)', () => { - const expectedActionFrequency = 'Hourly'; + const expectedActionFrequency: RuleActionCustomFrequency = { + throttle: 2, + throttleUnit: 'h', + }; const expectedEmail = 'test@example.com'; const expectedSubject = 'Subject'; selectNumberOfRules(expectedNumberOfCustomRulesToBeEdited); openBulkEditRuleActionsForm(); - pickActionFrequency(expectedActionFrequency); addEmailConnectorAndRuleAction(expectedEmail, expectedSubject); + pickSummaryOfAlertsOption(); + pickCustomFrequencyOption(expectedActionFrequency); submitBulkEditForm(); waitForBulkEditActionToFinish({ updatedCount: expectedNumberOfCustomRulesToBeEdited }); @@ -194,7 +214,8 @@ describe.skip('Detection rules, bulk edit of rule actions', () => { // check if rule has been updated goToEditRuleActionsSettingsOf(ruleNameToAssert); - assertSelectedActionFrequency(expectedActionFrequency); + assertSelectedSummaryOfAlertsOption(); + assertSelectedCustomFrequencyOption(expectedActionFrequency, 1); assertEmailRuleAction(expectedEmail, expectedSubject); }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts index 8e2ae1b85cce7..4965072e7038d 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/custom_query_rule.cy.ts @@ -19,10 +19,13 @@ import { RULE_SWITCH, SEVERITY, } from '../../screens/alerts_detection_rules'; +import { + ACTIONS_NOTIFY_WHEN_BUTTON, + ACTIONS_SUMMARY_BUTTON, +} from '../../screens/common/rule_actions'; import { ABOUT_CONTINUE_BTN, ABOUT_EDIT_BUTTON, - ACTIONS_THROTTLE_INPUT, CUSTOM_QUERY_INPUT, DEFINE_CONTINUE_BUTTON, DEFINE_EDIT_BUTTON, @@ -401,12 +404,11 @@ describe('Custom query rules', () => { goToActionsStepTab(); - cy.get(ACTIONS_THROTTLE_INPUT).invoke('val').should('eql', 'no_actions'); - - cy.get(ACTIONS_THROTTLE_INPUT).select('Weekly'); - addEmailConnectorAndRuleAction('test@example.com', 'Subject'); + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts'); + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run'); + goToAboutStepTab(); cy.get(TAGS_CLEAR_BUTTON).click({ force: true }); fillAboutRule(getEditedRule()); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/indicator_match_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/indicator_match_rule.cy.ts index 740bdcfb54dac..fbc28e8a2c671 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/indicator_match_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/indicator_match_rule.cy.ts @@ -58,12 +58,12 @@ import { INDICATOR_MATCH_ROW_RENDER, PROVIDER_BADGE } from '../../screens/timeli import { investigateFirstAlertInTimeline } from '../../tasks/alerts'; import { duplicateFirstRule, - duplicateSelectedRules, duplicateRuleFromMenu, goToRuleDetails, selectNumberOfRules, checkDuplicatedRule, expectNumberOfRules, + duplicateSelectedRulesWithExceptions, } from '../../tasks/alerts_detection_rules'; import { createRule } from '../../tasks/api_calls/rules'; import { loadPrepackagedTimelineTemplates } from '../../tasks/api_calls/timelines'; @@ -544,7 +544,7 @@ describe('indicator match', () => { it("Allows the rule to be duplicated from the table's bulk actions", () => { selectNumberOfRules(1); - duplicateSelectedRules(); + duplicateSelectedRulesWithExceptions(); checkDuplicatedRule(); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts index 5ed5ef8be059a..ab458e12dca2d 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_rules/rule_actions.cy.ts @@ -43,7 +43,7 @@ describe('Rule actions during detection rule creation', () => { }); const rule = getSimpleCustomQueryRule(); - const actions = { throttle: 'rule', connectors: [indexConnector] }; + const actions = { connectors: [indexConnector] }; const index = actions.connectors[0].index; const initialNumberOfDocuments = 0; const expectedJson = JSON.parse(actions.connectors[0].document); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/add_exception.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/add_exception.cy.ts index 5964c8303ce1d..572e535d740fd 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/add_exception.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/alerts_table_flow/add_exception.cy.ts @@ -15,7 +15,7 @@ import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../.. import { EXCEPTIONS_URL } from '../../../urls/navigation'; import { deleteExceptionListWithRuleReferenceByListId, - deleteExceptionListWithoutRuleReference, + deleteExceptionListWithoutRuleReferenceByListId, exportExceptionList, searchForExceptionList, waitForExceptionsTableToBeLoaded, @@ -153,7 +153,7 @@ describe('Exceptions Table', () => { // just checking number of lists shown cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); - deleteExceptionListWithoutRuleReference(); + deleteExceptionListWithoutRuleReferenceByListId(getExceptionList1().list_id); // Using cy.contains because we do not care about the exact text, // just checking number of lists shown diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/all_exception_lists_read_only.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/all_exception_lists_read_only.cy.ts index bc6279113c8e5..d5e68594b9967 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/all_exception_lists_read_only.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/all_exception_lists_read_only.cy.ts @@ -9,7 +9,10 @@ import { esArchiverResetKibana } from '../../../tasks/es_archiver'; import { cleanKibana } from '../../../tasks/common'; import { ROLES } from '../../../../common/test'; import { getExceptionList } from '../../../objects/exception'; -import { EXCEPTIONS_TABLE_SHOWING_LISTS } from '../../../screens/exceptions'; +import { + EXCEPTIONS_OVERFLOW_ACTIONS_BTN, + EXCEPTIONS_TABLE_SHOWING_LISTS, +} from '../../../screens/exceptions'; import { createExceptionList, deleteExceptionList } from '../../../tasks/api_calls/exceptions'; import { dismissCallOut, @@ -56,4 +59,8 @@ describe('All exception lists - read only', () => { getCallOut(MISSING_PRIVILEGES_CALLOUT).should('not.exist'); }); }); + + it('Exception list actions should be disabled', () => { + cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().should('be.disabled'); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/manage_shared_exception_list.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/manage_shared_exception_list.cy.ts index f7610c3f4b5d9..bc3e732538ff0 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/manage_shared_exception_list.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/exceptions/shared_exception_lists_management/manage_shared_exception_list.cy.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { ROLES } from '../../../../common/test'; import { getExceptionList, expectedExportedExceptionList } from '../../../objects/exception'; import { getNewRule } from '../../../objects/rule'; @@ -14,8 +13,11 @@ import { login, visitWithoutDateRange, waitForPageWithoutDateRange } from '../.. import { EXCEPTIONS_URL } from '../../../urls/navigation'; import { + assertExceptionListsExists, + duplicateSharedExceptionListFromListsManagementPageByListId, + findSharedExceptionListItemsByName, + deleteExceptionListWithoutRuleReferenceByListId, deleteExceptionListWithRuleReferenceByListId, - deleteExceptionListWithoutRuleReference, exportExceptionList, waitForExceptionsTableToBeLoaded, createSharedExceptionList, @@ -24,135 +26,211 @@ import { } from '../../../tasks/exceptions_table'; import { EXCEPTIONS_LIST_MANAGEMENT_NAME, - EXCEPTIONS_OVERFLOW_ACTIONS_BTN, EXCEPTIONS_TABLE_SHOWING_LISTS, } from '../../../screens/exceptions'; -import { createExceptionList } from '../../../tasks/api_calls/exceptions'; +import { createExceptionList, createExceptionListItem } from '../../../tasks/api_calls/exceptions'; import { esArchiverResetKibana } from '../../../tasks/es_archiver'; +import { assertNumberOfExceptionItemsExists } from '../../../tasks/exceptions'; + import { TOASTER } from '../../../screens/alerts_detection_rules'; const EXCEPTION_LIST_NAME = 'My shared list'; +const EXCEPTION_LIST_TO_DUPLICATE_NAME = 'A test list 2'; +const EXCEPTION_LIST_ITEM_NAME = 'Sample Exception List Item 1'; +const EXCEPTION_LIST_ITEM_NAME_2 = 'Sample Exception List Item 2'; + const getExceptionList1 = () => ({ ...getExceptionList(), name: EXCEPTION_LIST_NAME, list_id: 'exception_list_1', }); + const getExceptionList2 = () => ({ ...getExceptionList(), - name: 'Test list 2', + name: EXCEPTION_LIST_TO_DUPLICATE_NAME, list_id: 'exception_list_2', }); +const expiredDate = new Date(Date.now() - 1000000).toISOString(); +const futureDate = new Date(Date.now() + 1000000).toISOString(); + describe('Manage shared exception list', () => { - before(() => { - esArchiverResetKibana(); - login(); - - createRule(getNewRule({ name: 'Another rule' })); - - // Create exception list associated with a rule - createExceptionList(getExceptionList2(), getExceptionList2().list_id).then((response) => - createRule( - getNewRule({ - exceptions_list: [ - { - id: response.body.id, - list_id: getExceptionList2().list_id, - type: getExceptionList2().type, - namespace_type: getExceptionList2().namespace_type, - }, - ], - }) - ) - ); - - // Create exception list not used by any rules - createExceptionList(getExceptionList1(), getExceptionList1().list_id).as( - 'exceptionListResponse' - ); - }); + describe('Create/Export/Delete', () => { + before(() => { + esArchiverResetKibana(); + login(); + + createRule(getNewRule({ name: 'Another rule' })); + + // Create exception list associated with a rule + createExceptionList(getExceptionList2(), getExceptionList2().list_id).then((response) => + createRule( + getNewRule({ + exceptions_list: [ + { + id: response.body.id, + list_id: getExceptionList2().list_id, + type: getExceptionList2().type, + namespace_type: getExceptionList2().namespace_type, + }, + ], + }) + ) + ); - beforeEach(() => { - visitWithoutDateRange(EXCEPTIONS_URL); - waitForExceptionsTableToBeLoaded(); - }); + // Create exception list not used by any rules + createExceptionList(getExceptionList1(), getExceptionList1().list_id).as( + 'exceptionListResponse' + ); + }); + + beforeEach(() => { + visitWithoutDateRange(EXCEPTIONS_URL); + waitForExceptionsTableToBeLoaded(); + }); - it('Export exception list', function () { - cy.intercept(/(\/api\/exception_lists\/_export)/).as('export'); + it('Export exception list', function () { + cy.intercept(/(\/api\/exception_lists\/_export)/).as('export'); - exportExceptionList(getExceptionList1().list_id); + exportExceptionList(getExceptionList1().list_id); - cy.wait('@export').then(({ response }) => { - cy.wrap(response?.body).should( - 'eql', - expectedExportedExceptionList(this.exceptionListResponse) - ); + cy.wait('@export').then(({ response }) => { + cy.wrap(response?.body).should( + 'eql', + expectedExportedExceptionList(this.exceptionListResponse) + ); + + cy.get(TOASTER).should( + 'have.text', + `Exception list "${EXCEPTION_LIST_NAME}" exported successfully` + ); + }); + }); + + it('Link rules to shared exception list', function () { + assertNumberLinkedRules(getExceptionList2().list_id, '1'); + linkRulesToExceptionList(getExceptionList2().list_id, 1); + assertNumberLinkedRules(getExceptionList2().list_id, '2'); + }); - cy.get(TOASTER).should( - 'have.text', - `Exception list "${EXCEPTION_LIST_NAME}" exported successfully` + it('Create exception list', function () { + createSharedExceptionList( + { name: 'Newly created list', description: 'This is my list.' }, + true ); + + // After creation - directed to list detail page + cy.get(EXCEPTIONS_LIST_MANAGEMENT_NAME).should('have.text', 'Newly created list'); }); - }); - it('Link rules to shared exception list', function () { - assertNumberLinkedRules(getExceptionList2().list_id, '1'); - linkRulesToExceptionList(getExceptionList2().list_id, 1); - assertNumberLinkedRules(getExceptionList2().list_id, '2'); - }); + it('Delete exception list without rule reference', () => { + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '4'); - it('Create exception list', function () { - createSharedExceptionList({ name: EXCEPTION_LIST_NAME, description: 'This is my list.' }, true); + deleteExceptionListWithoutRuleReferenceByListId(getExceptionList1().list_id); - // After creation - directed to list detail page - cy.get(EXCEPTIONS_LIST_MANAGEMENT_NAME).should('have.text', EXCEPTION_LIST_NAME); - }); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); + }); + + it('Deletes exception list with rule reference', () => { + waitForPageWithoutDateRange(EXCEPTIONS_URL); + waitForExceptionsTableToBeLoaded(); - it('Delete exception list without rule reference', () => { - // Using cy.contains because we do not care about the exact text, - // just checking number of lists shown - cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '4'); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); - deleteExceptionListWithoutRuleReference(); + deleteExceptionListWithRuleReferenceByListId(getExceptionList2().list_id); - // Using cy.contains because we do not care about the exact text, - // just checking number of lists shown - cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); + // Using cy.contains because we do not care about the exact text, + // just checking number of lists shown + cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '2'); + }); }); - it('Deletes exception list with rule reference', () => { - waitForPageWithoutDateRange(EXCEPTIONS_URL); - waitForExceptionsTableToBeLoaded(); + describe('Duplicate', () => { + beforeEach(() => { + esArchiverResetKibana(); + login(); + + // Create exception list associated with a rule + createExceptionList(getExceptionList2(), getExceptionList2().list_id); + + createExceptionListItem(getExceptionList2().list_id, { + list_id: getExceptionList2().list_id, + item_id: 'simple_list_item_1', + tags: [], + type: 'simple', + description: 'Test exception item', + name: EXCEPTION_LIST_ITEM_NAME, + namespace_type: 'single', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + expire_time: expiredDate, + }); + createExceptionListItem(getExceptionList2().list_id, { + list_id: getExceptionList2().list_id, + item_id: 'simple_list_item_2', + tags: [], + type: 'simple', + description: 'Test exception item', + name: EXCEPTION_LIST_ITEM_NAME_2, + namespace_type: 'single', + entries: [ + { + field: 'host.name', + operator: 'included', + type: 'match_any', + value: ['some host', 'another host'], + }, + ], + expire_time: futureDate, + }); + + visitWithoutDateRange(EXCEPTIONS_URL); + waitForExceptionsTableToBeLoaded(); + }); + + it('Duplicate exception list with expired items', function () { + duplicateSharedExceptionListFromListsManagementPageByListId( + getExceptionList2().list_id, + true + ); - // Using cy.contains because we do not care about the exact text, - // just checking number of lists shown - cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '3'); + // After duplication - check for new list + assertExceptionListsExists([`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`]); - deleteExceptionListWithRuleReferenceByListId(getExceptionList2().list_id); + findSharedExceptionListItemsByName(`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`, [ + EXCEPTION_LIST_ITEM_NAME, + EXCEPTION_LIST_ITEM_NAME_2, + ]); - // Using cy.contains because we do not care about the exact text, - // just checking number of lists shown - cy.contains(EXCEPTIONS_TABLE_SHOWING_LISTS, '2'); - }); -}); + assertNumberOfExceptionItemsExists(2); + }); -describe('Manage shared exception list - read only', () => { - before(() => { - // First we login as a privileged user to create exception list - esArchiverResetKibana(); - login(ROLES.platform_engineer); - visitWithoutDateRange(EXCEPTIONS_URL, ROLES.platform_engineer); - createExceptionList(getExceptionList(), getExceptionList().list_id); + it('Duplicate exception list without expired items', function () { + duplicateSharedExceptionListFromListsManagementPageByListId( + getExceptionList2().list_id, + false + ); - // Then we login as read-only user to test. - login(ROLES.reader); - visitWithoutDateRange(EXCEPTIONS_URL, ROLES.reader); - waitForExceptionsTableToBeLoaded(); + // After duplication - check for new list + assertExceptionListsExists([`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`]); - cy.get(EXCEPTIONS_TABLE_SHOWING_LISTS).should('have.text', `Showing 1 list`); - }); + findSharedExceptionListItemsByName(`${EXCEPTION_LIST_TO_DUPLICATE_NAME} [Duplicate]`, [ + EXCEPTION_LIST_ITEM_NAME_2, + ]); - it('Exception list actions should be disabled', () => { - cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().should('be.disabled'); + assertNumberOfExceptionItemsExists(1); + }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/exception.ts b/x-pack/plugins/security_solution/cypress/objects/exception.ts index 8427d828b6eea..d95455e34dc7e 100644 --- a/x-pack/plugins/security_solution/cypress/objects/exception.ts +++ b/x-pack/plugins/security_solution/cypress/objects/exception.ts @@ -31,6 +31,15 @@ export interface ExceptionListItem { tags: string[]; type: 'simple'; entries: Array<{ field: string; operator: string; type: string; value: string[] }>; + expire_time?: string; +} + +export interface RuleExceptionItem { + description: string; + name: string; + type: 'simple'; + entries: Array<{ field: string; operator: string; type: string; value: string[] | string }>; + expire_time: string | undefined; } export const getExceptionList = (): ExceptionList => ({ diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index ac630d1ee0fd9..af44aa14aa668 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -578,7 +578,6 @@ export const expectedExportedRule = (ruleResponse: Cypress.Response = Partial>; export interface Actions { - throttle: RuleActionThrottle; connectors: Connectors[]; } diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts index 2af59e2ae67d7..66f61cc0898f1 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_detection_rules.ts @@ -38,6 +38,13 @@ export const DELETE_RULE_BULK_BTN = '[data-test-subj="deleteRuleBulk"]'; export const DUPLICATE_RULE_BULK_BTN = '[data-test-subj="duplicateRuleBulk"]'; +export const DUPLICATE_WITH_EXCEPTIONS_OPTION = '[data-test-subj="withExceptions"] label'; + +export const DUPLICATE_WITH_EXCEPTIONS_WITHOUT_EXPIRED_OPTION = + '[data-test-subj="withExceptionsExcludeExpiredExceptions"] label'; + +export const DUPLICATE_WITHOUT_EXCEPTIONS_OPTION = '[data-test-subj="withoutExceptions"] label'; + export const RULE_SEARCH_FIELD = '[data-test-subj="ruleSearchField"]'; export const EXPORT_ACTION_BTN = '[data-test-subj="exportRuleAction"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts index 2fe606fc6bf64..9a1702b96d63c 100644 --- a/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/common/rule_actions.ts @@ -41,3 +41,20 @@ export const INDEX_SELECTOR = "[data-test-subj='.index-siem-ActionTypeSelectOpti export const actionFormSelector = (position: number) => `[data-test-subj="alertActionAccordion-${position}"]`; + +export const ACTIONS_SUMMARY_BUTTON = '[data-test-subj="summaryOrPerRuleSelect"]'; + +export const ACTIONS_NOTIFY_WHEN_BUTTON = '[data-test-subj="notifyWhenSelect"]'; + +export const ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON = '[data-test-subj="onActiveAlert"]'; + +export const ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON = '[data-test-subj="onThrottleInterval"]'; + +export const ACTIONS_THROTTLE_INPUT = '[data-test-subj="throttleInput"]'; + +export const ACTIONS_THROTTLE_UNIT_INPUT = '[data-test-subj="throttleUnitInput"]'; + +export const ACTIONS_SUMMARY_ALERT_BUTTON = '[data-test-subj="actionNotifyWhen-option-summary"]'; + +export const ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON = + '[data-test-subj="actionNotifyWhen-option-for_each"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index a0cccb508ed66..b248cf06e1a0d 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -13,9 +13,6 @@ export const ABOUT_EDIT_TAB = '[data-test-subj="edit-rule-about-tab"]'; export const ACTIONS_EDIT_TAB = '[data-test-subj="edit-rule-actions-tab"]'; -export const ACTIONS_THROTTLE_INPUT = - '[data-test-subj="stepRuleActions"] [data-test-subj="select"]'; - export const ADD_FALSE_POSITIVE_BTN = '[data-test-subj="detectionEngineStepAboutRuleFalsePositives"] .euiButtonEmpty__text'; diff --git a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts index 2f07705ab18d4..dafd16932d194 100644 --- a/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts +++ b/x-pack/plugins/security_solution/cypress/screens/document_expandable_flyout.ts @@ -73,6 +73,10 @@ import { ENTITIES_VIEW_ALL_BUTTON_TEST_ID, VISUALIZATIONS_SECTION_HEADER_TEST_ID, ANALYZER_TREE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, } from '../../public/flyout/right/components/test_ids'; import { getClassSelector, @@ -135,6 +139,8 @@ export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_BUTTON = getData ); export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_CONTENT = getDataTestSubjectSelector(SESSION_VIEW_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_SESSION_VIEW_NO_DATA = + getDataTestSubjectSelector('sessionView:sessionViewProcessEventsEmpty'); export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON = getDataTestSubjectSelector(VISUALIZE_TAB_GRAPH_ANALYZER_BUTTON_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_VISUALIZE_TAB_GRAPH_ANALYZER_CONTENT = @@ -301,6 +307,14 @@ export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_HEADER = getDataTestSubjectSelector(ENTITY_PANEL_HEADER_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_ENTITY_PANEL_CONTENT = getDataTestSubjectSelector(ENTITY_PANEL_CONTENT_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_HEADER = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_CONTENT = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VALUES = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID); +export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON = + getDataTestSubjectSelector(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID); export const DOCUMENT_DETAILS_FLYOUT_OVERVIEW_TAB_VISUALIZATIONS_SECTION_HEADER = getDataTestSubjectSelector(VISUALIZATIONS_SECTION_HEADER_TEST_ID); @@ -333,3 +347,6 @@ export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_ADD_TO_TIMELINE = getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-addToTimeline'); export const DOCUMENT_DETAILS_FLYOUT_TABLE_TAB_ROW_CELL_COPY_TO_CLIPBOARD = getDataTestSubjectSelector('actionItem-security-detailsFlyout-cellActions-copyToClipboard'); + +export const DOCUMENT_DETAILS_FLYOUT_CLOSE_BUTTON = + getDataTestSubjectSelector('euiFlyoutCloseButton'); diff --git a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts index 100ab6f91ab1d..6a58105292eff 100644 --- a/x-pack/plugins/security_solution/cypress/screens/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/screens/exceptions.ts @@ -55,7 +55,14 @@ export const EXCEPTIONS_TABLE_LINK_RULES_BTN = export const EXCEPTIONS_TABLE_EXPORT_MODAL_BTN = '[data-test-subj="sharedListOverflowCardActionItemExport"]'; -export const EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN = '[data-test-subj="confirmModalConfirmButton"]'; +export const EXCEPTIONS_TABLE_DUPLICATE_BTN = + '[data-test-subj="sharedListOverflowCardActionItemDuplicate"]'; + +export const EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN = + '[data-test-subj="confirmModalConfirmButton"]'; + +export const INCLUDE_EXPIRED_EXCEPTION_ITEMS_SWITCH = + '[data-test-subj="includeExpiredExceptionsConfirmationModalSwitch"]'; export const EXCEPTIONS_TABLE_SEARCH_CLEAR = '[data-test-subj="allExceptionListsPanel"] button.euiFormControlLayoutClearButton'; @@ -179,3 +186,5 @@ export const EXCEPTIONS_LIST_EDIT_DETAILS_SAVE_BTN = '[data-test-subj="editModal export const EXCEPTIONS_LIST_DETAILS_HEADER = '[data-test-subj="exceptionListManagementPageHeader"]'; + +export const EXCEPTION_LIST_DETAILS_CARD_ITEM_NAME = '[data-test-subj="exceptionItemCardHeader"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts index 5487c5be0aded..4abb1cad40d05 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rule_details.ts @@ -39,6 +39,12 @@ export const DETAILS_TITLE = '.euiDescriptionList__title'; export const EXCEPTIONS_TAB = 'a[data-test-subj="navigation-rule_exceptions"]'; +export const EXCEPTIONS_TAB_EXPIRED_FILTER = '[data-test-subj="expired"]'; + +export const EXCEPTIONS_TAB_ACTIVE_FILTER = '[data-test-subj="active"]'; + +export const EXCEPTIONS_ITEM_CONTAINER = '[data-test-subj="exceptionsContainer"]'; + export const FALSE_POSITIVES_DETAILS = 'False positive examples'; export const INDEX_PATTERNS_DETAILS = 'Index patterns'; diff --git a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts index 9546b5da8ad8d..7a36c7fd7c74e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/screens/rules_bulk_edit.ts @@ -66,9 +66,6 @@ export const UPDATE_SCHEDULE_LOOKBACK_INPUT = export const UPDATE_SCHEDULE_TIME_UNIT_SELECT = '[data-test-subj="timeType"]'; -export const RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT = - '[data-test-subj="bulkEditRulesRuleActionThrottle"] [data-test-subj="select"]'; - export const RULES_BULK_EDIT_ACTIONS_INFO = '[data-test-subj="bulkEditRulesRuleActionInfo"]'; export const RULES_BULK_EDIT_ACTIONS_WARNING = '[data-test-subj="bulkEditRulesRuleActionsWarning"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index f17fbac688f9d..3dd0f5d563597 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -62,6 +62,9 @@ import { DISABLED_RULES_BTN, REFRESH_RULES_TABLE_BUTTON, RULE_LAST_RUN, + DUPLICATE_WITHOUT_EXCEPTIONS_OPTION, + DUPLICATE_WITH_EXCEPTIONS_OPTION, + DUPLICATE_WITH_EXCEPTIONS_WITHOUT_EXPIRED_OPTION, } from '../screens/alerts_detection_rules'; import type { RULES_MONITORING_TABLE } from '../screens/alerts_detection_rules'; import { EUI_CHECKBOX } from '../screens/common/controls'; @@ -145,10 +148,27 @@ export const deleteRuleFromDetailsPage = () => { .should(($el) => expect($el).to.be.not.visible); }; -export const duplicateSelectedRules = () => { +export const duplicateSelectedRulesWithoutExceptions = () => { cy.log('Duplicate selected rules'); cy.get(BULK_ACTIONS_BTN).click({ force: true }); cy.get(DUPLICATE_RULE_BULK_BTN).click(); + cy.get(DUPLICATE_WITHOUT_EXCEPTIONS_OPTION).click(); + cy.get(CONFIRM_DUPLICATE_RULE).click(); +}; + +export const duplicateSelectedRulesWithExceptions = () => { + cy.log('Duplicate selected rules'); + cy.get(BULK_ACTIONS_BTN).click({ force: true }); + cy.get(DUPLICATE_RULE_BULK_BTN).click(); + cy.get(DUPLICATE_WITH_EXCEPTIONS_OPTION).click(); + cy.get(CONFIRM_DUPLICATE_RULE).click(); +}; + +export const duplicateSelectedRulesWithNonExpiredExceptions = () => { + cy.log('Duplicate selected rules'); + cy.get(BULK_ACTIONS_BTN).click({ force: true }); + cy.get(DUPLICATE_RULE_BULK_BTN).click(); + cy.get(DUPLICATE_WITH_EXCEPTIONS_WITHOUT_EXPIRED_OPTION).click(); cy.get(CONFIRM_DUPLICATE_RULE).click(); }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts index fd070cfcda55e..61c5a0aa8efc3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/exceptions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { ExceptionList, ExceptionListItem } from '../../objects/exception'; +import type { ExceptionList, ExceptionListItem, RuleExceptionItem } from '../../objects/exception'; export const createEndpointExceptionList = () => cy.request({ @@ -59,6 +59,18 @@ export const createExceptionListItem = ( value: ['some host', 'another host'], }, ], + expire_time: exceptionListItem?.expire_time, + }, + headers: { 'kbn-xsrf': 'cypress-creds' }, + failOnStatusCode: false, + }); + +export const createRuleExceptionItem = (ruleId: string, exceptionListItems: RuleExceptionItem[]) => + cy.request({ + method: 'POST', + url: `/api/detection_engine/rules/${ruleId}/exceptions`, + body: { + items: exceptionListItems, }, headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, diff --git a/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts index 2c289eea0f736..a9a45780567ae 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/common/rule_actions.ts @@ -22,6 +22,15 @@ import { EMAIL_CONNECTOR_PASSWORD_INPUT, FORM_VALIDATION_ERROR, JSON_EDITOR, + ACTIONS_SUMMARY_BUTTON, + ACTIONS_NOTIFY_WHEN_BUTTON, + ACTIONS_THROTTLE_INPUT, + ACTIONS_THROTTLE_UNIT_INPUT, + ACTIONS_SUMMARY_ALERT_BUTTON, + ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON, + ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON, + actionFormSelector, + ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON, } from '../../screens/common/rule_actions'; import { COMBO_BOX_INPUT, COMBO_BOX_SELECTION } from '../../screens/common/controls'; import type { EmailConnector, IndexConnector } from '../../objects/connector'; @@ -84,3 +93,79 @@ export const fillIndexConnectorForm = (connector: IndexConnector = getIndexConne parseSpecialCharSequences: false, }); }; + +export interface RuleActionCustomFrequency { + throttle?: number; + throttleUnit?: 's' | 'm' | 'h' | 'd'; +} + +export const pickSummaryOfAlertsOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).click(); + }); + cy.get(ACTIONS_SUMMARY_ALERT_BUTTON).click(); +}; +export const pickForEachAlertOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).click(); + }); + cy.get(ACTIONS_SUMMARY_FOR_EACH_ALERT_BUTTON).click(); +}; + +export const pickCustomFrequencyOption = ( + { throttle = 1, throttleUnit = 'h' }: RuleActionCustomFrequency, + index = 0 +) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).click(); + }); + cy.get(ACTIONS_NOTIFY_CUSTOM_FREQUENCY_BUTTON).click(); + form.within(() => { + cy.get(ACTIONS_THROTTLE_INPUT).type(`{selectAll}${throttle}`); + cy.get(ACTIONS_THROTTLE_UNIT_INPUT).select(throttleUnit); + }); +}; + +export const pickPerRuleRunFrequencyOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).click(); + }); + cy.get(ACTIONS_NOTIFY_PER_RULE_RUN_BUTTON).click(); +}; + +export const assertSelectedSummaryOfAlertsOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'Summary of alerts'); + }); +}; + +export const assertSelectedForEachAlertOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_SUMMARY_BUTTON).should('have.text', 'For each alert'); + }); +}; + +export const assertSelectedCustomFrequencyOption = ( + { throttle = 1, throttleUnit = 'h' }: RuleActionCustomFrequency, + index = 0 +) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Custom frequency'); + cy.get(ACTIONS_THROTTLE_INPUT).should('have.value', throttle); + cy.get(ACTIONS_THROTTLE_UNIT_INPUT).should('have.value', throttleUnit); + }); +}; + +export const assertSelectedPerRuleRunFrequencyOption = (index = 0) => { + const form = cy.get(actionFormSelector(index)); + form.within(() => { + cy.get(ACTIONS_NOTIFY_WHEN_BUTTON).should('have.text', 'Per rule run'); + }); +}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index b8274ed33c120..1f3f051c6f474 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -99,7 +99,6 @@ import { NEW_TERMS_HISTORY_SIZE, NEW_TERMS_HISTORY_TIME_TYPE, NEW_TERMS_INPUT_AREA, - ACTIONS_THROTTLE_INPUT, CONTINUE_BUTTON, CREATE_WITHOUT_ENABLING_BTN, RULE_INDICES, @@ -407,7 +406,6 @@ export const fillFrom = (from: RuleIntervalFrom = ruleFields.ruleIntervalFrom) = }; export const fillRuleAction = (actions: Actions) => { - cy.get(ACTIONS_THROTTLE_INPUT).select(actions.throttle); actions.connectors.forEach((connector) => { switch (connector.type) { case 'index': diff --git a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts index a016691328ffd..42d5619c28a67 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/edit_rule.ts @@ -6,7 +6,6 @@ */ import { BACK_TO_RULE_DETAILS, EDIT_SUBMIT_BUTTON } from '../screens/edit_rule'; -import { ACTIONS_THROTTLE_INPUT } from '../screens/create_new_rule'; export const saveEditedRule = () => { cy.get(EDIT_SUBMIT_BUTTON).should('exist').click({ force: true }); @@ -17,7 +16,3 @@ export const goBackToRuleDetails = () => { cy.get(BACK_TO_RULE_DETAILS).should('exist').click(); cy.get(BACK_TO_RULE_DETAILS).should('not.exist'); }; - -export const assertSelectedActionFrequency = (frequency: string) => { - cy.get(ACTIONS_THROTTLE_INPUT).find('option:selected').should('have.text', frequency); -}; diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts index e3e0d76ddd26a..c40176cd2eda4 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions.ts @@ -28,8 +28,24 @@ import { EXCEPTION_FIELD_MAPPING_CONFLICTS_TOOLTIP, EXCEPTION_FIELD_MAPPING_CONFLICTS_ACCORDION_ICON, EXCEPTION_FIELD_MAPPING_CONFLICTS_DESCRIPTION, + EXCEPTION_ITEM_VIEWER_CONTAINER, } from '../screens/exceptions'; +export const assertNumberOfExceptionItemsExists = (numberOfItems: number) => { + cy.get(EXCEPTION_ITEM_VIEWER_CONTAINER).should('have.length', numberOfItems); +}; + +export const expectToContainItem = (container: string, itemName: string) => { + cy.log(`Expecting exception items table to contain '${itemName}'`); + cy.get(container).should('include.text', itemName); +}; + +export const assertExceptionItemsExists = (container: string, itemNames: string[]) => { + for (const itemName of itemNames) { + expectToContainItem(container, itemName); + } +}; + export const addExceptionEntryFieldValueOfItemX = ( field: string, itemIndex = 0, diff --git a/x-pack/plugins/security_solution/cypress/tasks/exceptions_table.ts b/x-pack/plugins/security_solution/cypress/tasks/exceptions_table.ts index 0f7a8e60e8c45..c9117751a210e 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/exceptions_table.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/exceptions_table.ts @@ -14,7 +14,7 @@ import { EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN, EXCEPTIONS_TABLE_EXPORT_MODAL_BTN, EXCEPTIONS_OVERFLOW_ACTIONS_BTN, - EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN, + EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN, MANAGE_EXCEPTION_CREATE_BUTTON_MENU, MANAGE_EXCEPTION_CREATE_LIST_BUTTON, CREATE_SHARED_EXCEPTION_LIST_NAME_INPUT, @@ -26,12 +26,17 @@ import { EXCEPTIONS_LIST_MANAGEMENT_EDIT_MODAL_DESCRIPTION_INPUT, EXCEPTIONS_LIST_EDIT_DETAILS_SAVE_BTN, EXCEPTIONS_LIST_DETAILS_HEADER, + EXCEPTIONS_TABLE_DUPLICATE_BTN, + EXCEPTIONS_TABLE_LIST_NAME, + INCLUDE_EXPIRED_EXCEPTION_ITEMS_SWITCH, + EXCEPTION_LIST_DETAILS_CARD_ITEM_NAME, exceptionsTableListManagementListContainerByListId, EXCEPTIONS_TABLE_LINK_RULES_BTN, RULE_ACTION_LINK_RULE_SWITCH, LINKED_RULES_BADGE, MANAGE_RULES_SAVE, } from '../screens/exceptions'; +import { assertExceptionItemsExists } from './exceptions'; export const clearSearchSelection = () => { cy.get(EXCEPTIONS_TABLE_SEARCH_CLEAR).first().click(); @@ -46,7 +51,7 @@ export const exportExceptionList = (listId: string) => { .find(EXCEPTIONS_OVERFLOW_ACTIONS_BTN) .click(); cy.get(EXCEPTIONS_TABLE_EXPORT_MODAL_BTN).first().click(); - cy.get(EXCEPTIONS_TABLE_EXPORT_CONFIRM_BTN).first().click(); + cy.get(EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN).first().click(); }; export const assertNumberLinkedRules = (listId: string, numberOfRulesAsString: string) => { @@ -65,8 +70,10 @@ export const linkRulesToExceptionList = (listId: string, ruleSwitch: number = 0) cy.get(MANAGE_RULES_SAVE).first().click(); }; -export const deleteExceptionListWithoutRuleReference = () => { - cy.get(EXCEPTIONS_OVERFLOW_ACTIONS_BTN).first().click(); +export const deleteExceptionListWithoutRuleReferenceByListId = (listId: string) => { + cy.get(exceptionsTableListManagementListContainerByListId(listId)) + .find(EXCEPTIONS_OVERFLOW_ACTIONS_BTN) + .click(); cy.get(EXCEPTIONS_TABLE_DELETE_BTN).first().click(); cy.get(EXCEPTIONS_TABLE_MODAL).should('exist'); cy.get(EXCEPTIONS_TABLE_MODAL_CONFIRM_BTN).first().click(); @@ -117,10 +124,42 @@ export const createSharedExceptionList = ( } }; +export const expectToContainList = (listName: string) => { + cy.log(`Expecting exception lists table to contain '${listName}'`); + cy.get(EXCEPTIONS_TABLE_LIST_NAME).should('include.text', listName); +}; + +export const assertExceptionListsExists = (listNames: string[]) => { + for (const listName of listNames) { + expectToContainList(listName); + } +}; + +export const duplicateSharedExceptionListFromListsManagementPageByListId = ( + listId: string, + includeExpired: boolean +) => { + cy.log(`Duplicating list with list_id: '${listId}'`); + cy.get(exceptionsTableListManagementListContainerByListId(listId)) + .find(EXCEPTIONS_OVERFLOW_ACTIONS_BTN) + .click(); + cy.get(EXCEPTIONS_TABLE_DUPLICATE_BTN).first().click(); + if (!includeExpired) { + cy.get(INCLUDE_EXPIRED_EXCEPTION_ITEMS_SWITCH).first().click(); + } + cy.get(EXCEPTIONS_TABLE_EXPIRED_EXCEPTION_ITEMS_MODAL_CONFIRM_BTN).first().click(); +}; + export const waitForExceptionListDetailToBeLoaded = () => { cy.get(EXCEPTIONS_LIST_DETAILS_HEADER).should('exist'); }; +export const findSharedExceptionListItemsByName = (listName: string, itemNames: string[]) => { + cy.contains(listName).click(); + waitForExceptionListDetailToBeLoaded(); + assertExceptionItemsExists(EXCEPTION_LIST_DETAILS_CARD_ITEM_NAME, itemNames); +}; + export const editExceptionLisDetails = ({ name, description, diff --git a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts index d30cbaf06e17b..d82e29e493ea3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rule_details.ts @@ -29,6 +29,8 @@ import { ENDPOINT_EXCEPTIONS_TAB, EDIT_RULE_SETTINGS_LINK, BACK_TO_RULES_TABLE, + EXCEPTIONS_TAB_EXPIRED_FILTER, + EXCEPTIONS_TAB_ACTIVE_FILTER, } from '../screens/rule_details'; import { addExceptionConditions, @@ -105,6 +107,11 @@ export const goToExceptionsTab = () => { cy.get(EXCEPTIONS_TAB).click(); }; +export const viewExpiredExceptionItems = () => { + cy.get(EXCEPTIONS_TAB_EXPIRED_FILTER).click(); + cy.get(EXCEPTIONS_TAB_ACTIVE_FILTER).click(); +}; + export const goToEndpointExceptionsTab = () => { cy.get(ENDPOINT_EXCEPTIONS_TAB).should('exist'); cy.get(ENDPOINT_EXCEPTIONS_TAB).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts index b2203d1b1202a..b4bf088bafd6a 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/rules_bulk_edit.ts @@ -39,7 +39,6 @@ import { UPDATE_SCHEDULE_LOOKBACK_INPUT, RULES_BULK_EDIT_SCHEDULES_WARNING, RULES_BULK_EDIT_OVERWRITE_ACTIONS_CHECKBOX, - RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT, } from '../screens/rules_bulk_edit'; import { SCHEDULE_DETAILS } from '../screens/rule_details'; @@ -292,7 +291,3 @@ export const assertRuleScheduleValues = ({ interval, lookback }: RuleSchedule) = cy.get('dd').eq(1).should('contain.text', lookback); }); }; - -export const pickActionFrequency = (frequency: string) => { - cy.get(RULES_BULK_EDIT_ACTIONS_THROTTLE_INPUT).select(frequency); -}; diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.test.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.test.ts new file mode 100644 index 0000000000000..7f3ed646da749 --- /dev/null +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.test.ts @@ -0,0 +1,247 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { SecurityAppStore } from '../../../common/store/types'; +import type { DataProvider } from '../../../../common/types'; +import { TimelineId } from '../../../../common/types'; +import { addProvider } from '../../../timelines/store/timeline/actions'; +import { createAddToNewTimelineCellActionFactory, getToastMessage } from './add_to_new_timeline'; +import type { CellActionExecutionContext } from '@kbn/cell-actions'; +import { GEO_FIELD_TYPE } from '../../../timelines/components/timeline/body/renderers/constants'; +import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; +import { timelineActions } from '../../../timelines/store/timeline'; + +const services = createStartServicesMock(); +const mockWarningToast = services.notifications.toasts.addWarning; + +const mockDispatch = jest.fn(); +const store = { + dispatch: mockDispatch, +} as unknown as SecurityAppStore; + +const value = 'the-value'; + +const context = { + field: { name: 'user.name', value, type: 'text' }, +} as CellActionExecutionContext; + +const defaultAddProviderAction = { + type: addProvider.type, + payload: { + id: TimelineId.active, + providers: [ + { + and: [], + enabled: true, + excluded: false, + id: 'event-field-default-timeline-1-user_name-0-the-value', + kqlQuery: '', + name: 'user.name', + queryMatch: { + field: 'user.name', + operator: ':', + value: 'the-value', + }, + }, + ], + }, +}; + +describe('createAddToNewTimelineCellAction', () => { + const addToTimelineCellActionFactory = createAddToNewTimelineCellActionFactory({ + store, + services, + }); + const addToTimelineAction = addToTimelineCellActionFactory({ id: 'testAddToTimeline', order: 1 }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return display name', () => { + expect(addToTimelineAction.getDisplayName(context)).toEqual('Investigate in timeline'); + }); + + it('should return icon type', () => { + expect(addToTimelineAction.getIconType(context)).toEqual('timeline'); + }); + + describe('isCompatible', () => { + it('should return true if everything is okay', async () => { + expect(await addToTimelineAction.isCompatible(context)).toEqual(true); + }); + it('should return false if field not allowed', async () => { + expect( + await addToTimelineAction.isCompatible({ + ...context, + field: { ...context.field, name: 'signal.reason' }, + }) + ).toEqual(false); + }); + }); + + describe('execute', () => { + it('should execute normally', async () => { + await addToTimelineAction.execute(context); + expect(mockDispatch).toHaveBeenCalledWith(defaultAddProviderAction); + expect(mockWarningToast).not.toHaveBeenCalled(); + }); + + it('should show warning if no provider added', async () => { + await addToTimelineAction.execute({ + ...context, + field: { + ...context.field, + type: GEO_FIELD_TYPE, + }, + }); + expect(mockDispatch).not.toHaveBeenCalled(); + expect(mockWarningToast).toHaveBeenCalled(); + }); + + describe('should execute correctly when negateFilters is provided', () => { + it('should not exclude if negateFilters is false', async () => { + await addToTimelineAction.execute({ + ...context, + metadata: { + negateFilters: false, + }, + }); + expect(mockDispatch).toHaveBeenCalledWith(defaultAddProviderAction); + expect(mockWarningToast).not.toHaveBeenCalled(); + }); + + it('should exclude if negateFilters is true', async () => { + await addToTimelineAction.execute({ + ...context, + metadata: { + negateFilters: true, + }, + }); + expect(mockDispatch).toHaveBeenCalledWith({ + ...defaultAddProviderAction, + payload: { + ...defaultAddProviderAction.payload, + providers: [{ ...defaultAddProviderAction.payload.providers[0], excluded: true }], + }, + }); + expect(mockWarningToast).not.toHaveBeenCalled(); + }); + }); + + it('should clear the timeline', async () => { + await addToTimelineAction.execute(context); + expect(mockDispatch.mock.calls[0][0].type).toEqual(timelineActions.createTimeline.type); + }); + + it('should add the providers to the timeline', async () => { + await addToTimelineAction.execute({ + ...context, + metadata: { + andFilters: [{ field: 'kibana.alert.severity', value: 'low' }], + }, + }); + + expect(mockDispatch).toBeCalledWith({ + ...defaultAddProviderAction, + payload: { + ...defaultAddProviderAction.payload, + providers: [ + { + ...defaultAddProviderAction.payload.providers[0], + id: 'event-field-default-timeline-1-user_name-0-the-value', + queryMatch: defaultAddProviderAction.payload.providers[0].queryMatch, + and: [ + { + enabled: true, + excluded: false, + id: 'event-field-default-timeline-1-kibana_alert_severity-0-low', + kqlQuery: '', + name: 'kibana.alert.severity', + queryMatch: { + field: 'kibana.alert.severity', + operator: ':', + value: 'low', + }, + and: [], + }, + ], + }, + ], + }, + }); + }); + }); + + describe('getToastMessage', () => { + it('handles empty input', () => { + const result = getToastMessage({ queryMatch: { value: null } } as unknown as DataProvider); + expect(result).toEqual(''); + }); + it('handles array input', () => { + const result = getToastMessage({ + queryMatch: { value: ['hello', 'world'] }, + } as unknown as DataProvider); + expect(result).toEqual('hello, world alerts'); + }); + + it('handles single filter', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [{ queryMatch: { field: 'kibana.alert.severity', value: 'critical' } }], + } as unknown as DataProvider); + expect(result).toEqual(`critical severity alerts from ${value}`); + }); + + it('handles multiple filters', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [ + { + queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' }, + }, + { + queryMatch: { field: 'kibana.alert.severity', value: 'critical' }, + }, + ], + } as unknown as DataProvider); + expect(result).toEqual(`open, critical severity alerts from ${value}`); + }); + + it('ignores unrelated filters', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [ + { + queryMatch: { field: 'kibana.alert.workflow_status', value: 'open' }, + }, + { + queryMatch: { field: 'kibana.alert.severity', value: 'critical' }, + }, + // currently only supporting the above fields + { + queryMatch: { field: 'user.name', value: 'something' }, + }, + ], + } as unknown as DataProvider); + expect(result).toEqual(`open, critical severity alerts from ${value}`); + }); + + it('returns entity only when unrelated filters are passed', () => { + const result = getToastMessage({ + queryMatch: { value }, + and: [{ queryMatch: { field: 'user.name', value: 'something' } }], + } as unknown as DataProvider); + expect(result).toEqual(`${value} alerts`); + }); + + it('returns entity only when no filters are passed', () => { + const result = getToastMessage({ queryMatch: { value }, and: [] } as unknown as DataProvider); + expect(result).toEqual(`${value} alerts`); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts new file mode 100644 index 0000000000000..886162803bbf1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/cell_action/add_to_new_timeline.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createCellActionFactory, type CellActionTemplate } from '@kbn/cell-actions'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { addProvider } from '../../../timelines/store/timeline/actions'; +import type { DataProvider } from '../../../../common/types'; +import { TimelineId } from '../../../../common/types'; +import type { SecurityAppStore } from '../../../common/store'; +import { fieldHasCellActions } from '../../utils'; +import { + ADD_TO_NEW_TIMELINE, + ADD_TO_TIMELINE_FAILED_TEXT, + ADD_TO_TIMELINE_FAILED_TITLE, + ADD_TO_TIMELINE_ICON, + ADD_TO_TIMELINE_SUCCESS_TITLE, + ALERTS_COUNT, + SEVERITY, +} from '../constants'; +import { createDataProviders, isValidDataProviderField } from '../data_provider'; +import { SecurityCellActionType } from '../../constants'; +import type { StartServices } from '../../../types'; +import type { SecurityCellAction } from '../../types'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; + +const severityField = 'kibana.alert.severity'; +const statusField = 'kibana.alert.workflow_status'; + +export const getToastMessage = ({ queryMatch: { value }, and = [] }: DataProvider) => { + if (value == null) { + return ''; + } + const fieldValue = Array.isArray(value) ? value.join(', ') : value.toString(); + + const descriptors = and.reduce((msg, { queryMatch }) => { + if (Array.isArray(queryMatch.value)) { + return msg; + } + if (queryMatch.field === severityField) { + msg.push(SEVERITY(queryMatch.value.toString())); + } + if (queryMatch.field === statusField) { + msg.push(queryMatch.value.toString()); + } + return msg; + }, []); + + return ALERTS_COUNT(fieldValue, descriptors.join(', ')); +}; + +export const createAddToNewTimelineCellActionFactory = createCellActionFactory( + ({ + store, + services, + }: { + store: SecurityAppStore; + services: StartServices; + }): CellActionTemplate => { + const { notifications: notificationsService } = services; + + return { + type: SecurityCellActionType.ADD_TO_TIMELINE, + getIconType: () => ADD_TO_TIMELINE_ICON, + getDisplayName: () => ADD_TO_NEW_TIMELINE, + getDisplayNameTooltip: () => ADD_TO_NEW_TIMELINE, + isCompatible: async ({ field }) => + fieldHasCellActions(field.name) && isValidDataProviderField(field.name, field.type), + execute: async ({ field, metadata }) => { + const dataProviders = + createDataProviders({ + contextId: TimelineId.active, + fieldType: field.type, + values: field.value, + field: field.name, + negate: metadata?.negateFilters === true, + }) ?? []; + + for (const andFilter of metadata?.andFilters ?? []) { + const andDataProviders = + createDataProviders({ + contextId: TimelineId.active, + field: andFilter.field, + values: andFilter.value, + }) ?? []; + if (andDataProviders) { + for (const dataProvider of dataProviders) { + dataProvider.and.push(...andDataProviders); + } + } + } + + if (dataProviders.length > 0) { + // clear timeline + store.dispatch( + timelineActions.createTimeline({ + ...timelineDefaults, + id: TimelineId.active, + }) + ); + store.dispatch(addProvider({ id: TimelineId.active, providers: dataProviders })); + notificationsService.toasts.addSuccess({ + title: ADD_TO_TIMELINE_SUCCESS_TITLE(getToastMessage(dataProviders[0])), + }); + } else { + notificationsService.toasts.addWarning({ + title: ADD_TO_TIMELINE_FAILED_TITLE, + text: ADD_TO_TIMELINE_FAILED_TEXT, + }); + } + }, + }; + } +); diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts index c122d7e312e4d..0396cad110367 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/constants.ts @@ -15,6 +15,29 @@ export const ADD_TO_TIMELINE = i18n.translate( defaultMessage: 'Add to timeline', } ); +export const ADD_TO_NEW_TIMELINE = i18n.translate( + 'xpack.securitySolution.actions.cellValue.addToNewTimeline.displayName', + { + defaultMessage: 'Investigate in timeline', + } +); + +export const SEVERITY = (level: string) => + i18n.translate('xpack.securitySolution.actions.addToTimeline.severityLevel', { + values: { level }, + defaultMessage: `{level} severity`, + }); + +export const ALERTS_COUNT = (entity: string, description: string) => + description !== '' + ? i18n.translate('xpack.securitySolution.actions.addToTimeline.descriptiveAlertsCountMessage', { + values: { description, entity }, + defaultMessage: '{description} alerts from {entity}', + }) + : i18n.translate('xpack.securitySolution.actions.addToTimeline.alertsCountMessage', { + values: { entity }, + defaultMessage: '{entity} alerts', + }); export const ADD_TO_TIMELINE_SUCCESS_TITLE = (value: string) => i18n.translate('xpack.securitySolution.actions.addToTimeline.addedFieldMessage', { diff --git a/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts b/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts index c639dde1e2337..72e6eee17e4d4 100644 --- a/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts +++ b/x-pack/plugins/security_solution/public/actions/add_to_timeline/index.ts @@ -6,4 +6,5 @@ */ export { createAddToTimelineCellActionFactory } from './cell_action/add_to_timeline'; +export { createAddToNewTimelineCellActionFactory } from './cell_action/add_to_new_timeline'; export { createAddToTimelineLensAction } from './lens/add_to_timeline'; diff --git a/x-pack/plugins/security_solution/public/actions/constants.ts b/x-pack/plugins/security_solution/public/actions/constants.ts index 94c2601222847..eff71171fbcea 100644 --- a/x-pack/plugins/security_solution/public/actions/constants.ts +++ b/x-pack/plugins/security_solution/public/actions/constants.ts @@ -7,6 +7,7 @@ export enum SecurityCellActionsTrigger { DEFAULT = 'security-default-cellActions', DETAILS_FLYOUT = 'security-detailsFlyout-cellActions', + ALERTS_COUNT = 'security-alertsCount-cellActions', } export enum SecurityCellActionType { diff --git a/x-pack/plugins/security_solution/public/actions/register.ts b/x-pack/plugins/security_solution/public/actions/register.ts index 2df581342f614..bfea8d27163c4 100644 --- a/x-pack/plugins/security_solution/public/actions/register.ts +++ b/x-pack/plugins/security_solution/public/actions/register.ts @@ -13,6 +13,7 @@ import { createFilterInCellActionFactory, createFilterOutCellActionFactory } fro import { createAddToTimelineLensAction, createAddToTimelineCellActionFactory, + createAddToNewTimelineCellActionFactory, } from './add_to_timeline'; import { createShowTopNCellActionFactory } from './show_top_n'; import { @@ -52,6 +53,7 @@ const registerCellActions = ( filterIn: createFilterInCellActionFactory({ store, services }), filterOut: createFilterOutCellActionFactory({ store, services }), addToTimeline: createAddToTimelineCellActionFactory({ store, services }), + addToNewTimeline: createAddToNewTimelineCellActionFactory({ store, services }), showTopN: createShowTopNCellActionFactory({ store, history, services }), copyToClipboard: createCopyToClipboardCellActionFactory({ services }), toggleColumn: createToggleColumnCellActionFactory({ store }), @@ -77,6 +79,13 @@ const registerCellActions = ( ], services, }); + + registerCellActionsTrigger({ + triggerId: SecurityCellActionsTrigger.ALERTS_COUNT, + cellActions, + actionsOrder: ['addToNewTimeline'], + services, + }); }; const registerCellActionsTrigger = ({ @@ -95,8 +104,10 @@ const registerCellActionsTrigger = ({ actionsOrder.forEach((actionName, order) => { const actionFactory = cellActions[actionName]; - const action = actionFactory({ id: `${triggerId}-${actionName}`, order }); + if (actionFactory) { + const action = actionFactory({ id: `${triggerId}-${actionName}`, order }); - uiActions.addTriggerAction(triggerId, enhanceActionWithTelemetry(action, services)); + uiActions.addTriggerAction(triggerId, enhanceActionWithTelemetry(action, services)); + } }); }; diff --git a/x-pack/plugins/security_solution/public/actions/types.ts b/x-pack/plugins/security_solution/public/actions/types.ts index 1d15896ae6b35..e0cd6e764f3b8 100644 --- a/x-pack/plugins/security_solution/public/actions/types.ts +++ b/x-pack/plugins/security_solution/public/actions/types.ts @@ -6,6 +6,12 @@ */ import type { CellAction, CellActionExecutionContext, CellActionFactory } from '@kbn/cell-actions'; +import type { QueryOperator } from '../../common/types'; +export interface AndFilter { + field: string; + value: string | string[]; + operator?: QueryOperator; +} export interface SecurityMetadata extends Record { /** @@ -35,6 +41,11 @@ export interface SecurityMetadata extends Record { */ component: string; }; + /** + * `metadata.andFilters` is used by the addToTimelineAction to add + * an "and" query to the main data provider + */ + andFilters?: AndFilter[]; } export interface SecurityCellActionExecutionContext extends CellActionExecutionContext { @@ -42,13 +53,15 @@ export interface SecurityCellActionExecutionContext extends CellActionExecutionC } export type SecurityCellAction = CellAction; -// All security cell actions names -export type SecurityCellActionName = - | 'filterIn' - | 'filterOut' - | 'addToTimeline' - | 'showTopN' - | 'copyToClipboard' - | 'toggleColumn'; +export interface SecurityCellActions { + filterIn?: CellActionFactory; + filterOut?: CellActionFactory; + addToTimeline?: CellActionFactory; + addToNewTimeline?: CellActionFactory; + showTopN?: CellActionFactory; + copyToClipboard?: CellActionFactory; + toggleColumn?: CellActionFactory; +} -export type SecurityCellActions = Record; +// All security cell actions names +export type SecurityCellActionName = keyof SecurityCellActions; diff --git a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx index 264df10831fb7..a70a915e6aed4 100644 --- a/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/template_wrapper/index.tsx @@ -27,6 +27,7 @@ import { useShowTimeline } from '../../../common/utils/timeline/use_show_timelin import { useShowPagesWithEmptyView } from '../../../common/utils/empty_view/use_show_pages_with_empty_view'; import { useIsPolicySettingsBarVisible } from '../../../management/pages/policy/view/policy_hooks'; import { useIsGroupedNavigationEnabled } from '../../../common/components/navigation/helpers'; +import { useSyncFlyoutStateWithUrl } from '../../../flyout/url/use_sync_flyout_state_with_url'; const NO_DATA_PAGE_MAX_WIDTH = 950; @@ -75,6 +76,8 @@ export const SecuritySolutionTemplateWrapper: React.FC + )} - {}} /> + {}} + handleOnFlyoutClosed={handleFlyoutChangedOrClosed} + /> ); diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx index 5a3f4b3e25e0e..07342c4f60691 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { SecurityPageName } from '../../../../common/constants'; -import { useGlobalTime } from '../../containers/use_global_time'; import { DEFAULT_STACK_BY_FIELD, DEFAULT_STACK_BY_FIELD1, @@ -151,16 +150,6 @@ describe('AlertsTreemapPanel', () => { await waitFor(() => expect(screen.getByTestId('treemapPanel')).toBeInTheDocument()); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - render( - - - - ); - - await waitFor(() => expect(useGlobalTime).toBeCalledWith(false)); - }); - it('renders the panel with a hidden overflow-x', async () => { render( diff --git a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx index a3ba582b3c974..33e526932c3e6 100644 --- a/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/alerts_treemap_panel/index.tsx @@ -80,7 +80,7 @@ const AlertsTreemapPanelComponent: React.FC = ({ stackByWidth, title, }: Props) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${ALERTS_TREEMAP_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap index 941078e41f917..4cc7aad784cc8 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/__snapshots__/entity.test.tsx.snap @@ -10,7 +10,7 @@ exports[`entity_draggable renders correctly against snapshot 1`] = ` "value": "entity-value", } } - mode="hover" + mode="hover-down" triggerId="security-default-cellActions" visibleCellActions={5} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx index 00a8c0cdb3f39..a50ea1e1f47f5 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/entity.tsx @@ -23,7 +23,7 @@ export const EntityComponent: React.FC = ({ entityName, entityValue }) => aggregatable: true, }} triggerId={SecurityCellActionsTrigger.DEFAULT} - mode={CellActionsMode.HOVER} + mode={CellActionsMode.HOVER_DOWN} visibleCellActions={5} > {`${entityName}: "${entityValue}"`} diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap index 08e1bbe2bfc80..2d9d6a69af1f0 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/__snapshots__/score.test.tsx.snap @@ -10,7 +10,7 @@ exports[`draggable_score renders correctly against snapshot 1`] = ` "value": "du", } } - mode="hover" + mode="hover-down" triggerId="security-default-cellActions" visibleCellActions={5} > @@ -28,7 +28,7 @@ exports[`draggable_score renders correctly against snapshot when the index is no "value": "du", } } - mode="hover" + mode="hover-down" triggerId="security-default-cellActions" visibleCellActions={5} > diff --git a/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx index 3ad058dd2ab33..4e5fc8b29190b 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/score/score.tsx @@ -26,7 +26,7 @@ export const ScoreComponent = ({ return ( = ({ value, }) => { const { uiSettings } = useKibana().services; - const { from, deleteQuery, setQuery, to } = useGlobalTime(false); + const { from, deleteQuery, setQuery, to } = useGlobalTime(); const options = getOptions(isActiveTimeline(scopeId ?? '') ? activeTimelineEventType : undefined); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap index b6177143f024c..8a37b8b9fe54b 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/__snapshots__/risk_score_over_time_area.test.ts.snap @@ -184,7 +184,7 @@ Object { "color": "#aa6556", "fill": "none", "forAccessor": "1dd5663b-f062-43f8-8688-fc8166c2ca8e", - "icon": "warning", + "icon": "alert", "iconPosition": "left", "lineWidth": 2, "textVisibility": true, diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts index 08cd3d131d166..97dbe4ea17e48 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.test.ts @@ -6,6 +6,7 @@ */ import { renderHook } from '@testing-library/react-hooks'; +import type { XYState } from '@kbn/lens-plugin/public'; import { wrapper } from '../../../mocks'; import { useLensAttributes } from '../../../use_lens_attributes'; @@ -56,4 +57,42 @@ describe('getRiskScoreOverTimeAreaAttributes', () => { expect(result?.current).toMatchSnapshot(); }); + + it('should render a Reference Line with an Alert icon', () => { + const { result } = renderHook( + () => + useLensAttributes({ + getLensAttributes: getRiskScoreOverTimeAreaAttributes, + stackByField: 'host', + extraOptions: { + spaceId: 'mockSpaceId', + }, + }), + { wrapper } + ); + + expect( + (result?.current?.state.visualization as XYState).layers.find( + (layer) => layer.layerType === 'referenceLine' + ) + ).toEqual( + expect.objectContaining({ + layerId: '1dd5663b-f062-43f8-8688-fc8166c2ca8e', + layerType: 'referenceLine', + accessors: ['1dd5663b-f062-43f8-8688-fc8166c2ca8e'], + yConfig: [ + { + forAccessor: '1dd5663b-f062-43f8-8688-fc8166c2ca8e', + axisMode: 'left', + lineWidth: 2, + color: '#aa6556', + icon: 'alert', + textVisibility: true, + fill: 'none', + iconPosition: 'left', + }, + ], + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts index 8c5981d6e4a06..b100e5042a33a 100644 --- a/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts +++ b/x-pack/plugins/security_solution/public/common/components/visualization_actions/lens_attributes/common/risk_scores/risk_score_over_time_area.ts @@ -56,7 +56,7 @@ export const getRiskScoreOverTimeAreaAttributes: GetLensAttributes = ( axisMode: 'left', lineWidth: 2, color: '#aa6556', - icon: 'warning', + icon: 'alert', textVisibility: true, fill: 'none', iconPosition: 'left', diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts deleted file mode 100644 index b1a21d9ae492d..0000000000000 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.test.ts +++ /dev/null @@ -1,46 +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 { renderHook } from '@testing-library/react-hooks'; - -import { TestProviders } from '../../mock'; -import { useAlertPrevalence } from './use_alert_prevalence'; -import { useGlobalTime } from '../use_global_time'; - -const from = '2022-07-28T08:20:18.966Z'; -const to = '2022-07-28T08:20:18.966Z'; -jest.mock('../use_global_time', () => { - const actual = jest.requireActual('../use_global_time'); - return { - ...actual, - useGlobalTime: jest - .fn() - .mockReturnValue({ from, to, setQuery: jest.fn(), deleteQuery: jest.fn() }), - }; -}); - -describe('useAlertPrevalence', () => { - beforeEach(() => jest.resetAllMocks()); - - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', () => { - renderHook( - () => - useAlertPrevalence({ - field: 'host.name', - value: ['Host-byc3w6qlpo'], - isActiveTimelines: false, - signalIndexName: null, - includeAlertIds: false, - }), - { - wrapper: TestProviders, - } - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); -}); diff --git a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts index 03ac3d6169351..3e98e067bfe2d 100644 --- a/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts +++ b/x-pack/plugins/security_solution/public/common/containers/alerts/use_alert_prevalence.ts @@ -44,7 +44,7 @@ export const useAlertPrevalence = ({ const timelineTime = useDeepEqualSelector((state) => inputsSelectors.timelineTimeRangeSelector(state) ); - const globalTime = useGlobalTime(false); + const globalTime = useGlobalTime(); let to: string | undefined; let from: string | undefined; if (ignoreTimerange === false) { diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx index 480ecdb3674ff..46a2738d6247a 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx @@ -37,23 +37,77 @@ describe('useGlobalTime', () => { expect(result1.to).toBe(0); }); - test('clear all queries at unmount when clearAllQuery is set to true', () => { - const { unmount } = renderHook(() => useGlobalTime()); + test('clear query at unmount when setQuery has been called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/local/inputs/DELETE_ALL_QUERY' + expect(mockDispatch.mock.calls.length).toBe(2); + expect(mockDispatch.mock.calls[1][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' ); }); - test('do NOT clear all queries at unmount when clearAllQuery is set to false.', () => { - const { unmount } = renderHook(() => useGlobalTime(false)); + test('do NOT clear query at unmount when setQuery has not been called', () => { + const { unmount } = renderHook(() => useGlobalTime()); unmount(); expect(mockDispatch.mock.calls.length).toBe(0); }); - test('do NOT clear all queries when setting state and clearAllQuery is set to true', () => { - const { rerender } = renderHook(() => useGlobalTime()); - act(() => rerender()); - expect(mockDispatch.mock.calls.length).toBe(0); + test('do clears only the dismounted queries at unmount when setQuery is called', () => { + const { result, unmount } = renderHook(() => useGlobalTime()); + + act(() => { + result.current.setQuery({ + id: 'query-1', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + act(() => { + result.current.setQuery({ + id: 'query-2', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + + const { result: theOneWillNotBeDismounted } = renderHook(() => useGlobalTime()); + + act(() => { + theOneWillNotBeDismounted.current.setQuery({ + id: 'query-3h', + inspect: { dsl: [], response: [] }, + loading: false, + refetch: () => {}, + searchSessionId: 'session-1', + }); + }); + unmount(); + expect(mockDispatch).toHaveBeenCalledTimes(5); + expect(mockDispatch.mock.calls[3][0].payload.id).toEqual('query-1'); + + expect(mockDispatch.mock.calls[3][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); + + expect(mockDispatch.mock.calls[4][0].payload.id).toEqual('query-2'); + + expect(mockDispatch.mock.calls[4][0].type).toEqual( + 'x-pack/security_solution/local/inputs/DELETE_QUERY' + ); }); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx index dbb57d57c3e6e..76cd23c8efba0 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx @@ -6,7 +6,7 @@ */ import { pick } from 'lodash/fp'; -import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useCallback, useState, useEffect, useMemo, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { InputsModelId } from '../../store/inputs/constants'; @@ -15,15 +15,18 @@ import { inputsSelectors } from '../../store'; import { inputsActions } from '../../store/actions'; import type { SetQuery, DeleteQuery } from './types'; -export const useGlobalTime = (clearAllQuery: boolean = true) => { +export const useGlobalTime = () => { const dispatch = useDispatch(); const { from, to } = useDeepEqualSelector((state) => pick(['from', 'to'], inputsSelectors.globalTimeRangeSelector(state)) ); const [isInitializing, setIsInitializing] = useState(true); + const queryId = useRef([]); + const setQuery = useCallback( - ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => + ({ id, inspect, loading, refetch, searchSessionId }: SetQuery) => { + queryId.current = [...queryId.current, id]; dispatch( inputsActions.setQuery({ inputId: InputsModelId.global, @@ -33,7 +36,8 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { refetch, searchSessionId, }) - ), + ); + }, [dispatch] ); @@ -50,13 +54,13 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { // This effect must not have any mutable dependencies. Otherwise, the cleanup function gets called before the component unmounts. useEffect(() => { return () => { - if (clearAllQuery) { - dispatch(inputsActions.deleteAllQuery({ id: InputsModelId.global })); + if (queryId.current.length > 0) { + queryId.current.forEach((id) => deleteQuery({ id })); } }; - }, [dispatch, clearAllQuery]); + }, [deleteQuery]); - const memoizedReturn = useMemo( + return useMemo( () => ({ isInitializing, from, @@ -66,8 +70,6 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { }), [deleteQuery, from, isInitializing, setQuery, to] ); - - return memoizedReturn; }; export type GlobalTimeArgs = Omit, 'deleteQuery'> & diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts index a5230c9ac599f..30d914f5ccc83 100644 --- a/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts +++ b/x-pack/plugins/security_solution/public/common/hooks/use_url_state.ts @@ -15,6 +15,9 @@ import { useQueryTimelineByIdOnUrlChange } from './timeline/use_query_timeline_b import { useInitFlyoutFromUrlParam } from './flyout/use_init_flyout_url_param'; import { useSyncFlyoutUrlParam } from './flyout/use_sync_flyout_url_param'; +// NOTE: the expandable flyout package url state is handled here: +// x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx + export const useUrlState = () => { useSyncGlobalQueryString(); useInitSearchBarFromUrlParams(); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts index a61186aeb0f8f..d78be8b03cb4d 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/actions.ts @@ -11,9 +11,5 @@ import type React from 'react'; const actionCreator = actionCreatorFactory('x-pack/security_solution/groups'); export const updateGroupSelector = actionCreator<{ - groupSelector: React.ReactElement; + groupSelector: React.ReactElement | null; }>('UPDATE_GROUP_SELECTOR'); - -export const updateSelectedGroup = actionCreator<{ - selectedGroup: string; -}>('UPDATE_SELECTED_GROUP'); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts index aaea793e4ca86..6914e4ad465fe 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/reducer.ts @@ -6,20 +6,17 @@ */ import { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { updateGroupSelector, updateSelectedGroup } from './actions'; +import { updateGroupSelector } from './actions'; import type { GroupModel } from './types'; export const initialGroupingState: GroupModel = { groupSelector: null, - selectedGroup: null, }; -export const groupsReducer = reducerWithInitialState(initialGroupingState) - .case(updateSelectedGroup, (state, { selectedGroup }) => ({ - ...state, - selectedGroup, - })) - .case(updateGroupSelector, (state, { groupSelector }) => ({ +export const groupsReducer = reducerWithInitialState(initialGroupingState).case( + updateGroupSelector, + (state, { groupSelector }) => ({ ...state, groupSelector, - })); + }) +); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts index eb63e256a4d9f..126fdac8c1b36 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/selectors.ts @@ -11,7 +11,3 @@ import type { GroupState } from './types'; const groupSelector = (state: GroupState) => state.groups.groupSelector; export const getGroupSelector = () => createSelector(groupSelector, (selector) => selector); - -export const selectedGroup = (state: GroupState) => state.groups.selectedGroup; - -export const getSelectedGroup = () => createSelector(selectedGroup, (group) => group); diff --git a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts index 7d8fd4bc3eeca..d2250b15722ed 100644 --- a/x-pack/plugins/security_solution/public/common/store/grouping/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/grouping/types.ts @@ -7,7 +7,6 @@ export interface GroupModel { groupSelector: React.ReactElement | null; - selectedGroup: string | null; } export interface GroupState { diff --git a/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx b/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx new file mode 100644 index 0000000000000..7fa16826eec60 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/components/rule_snooze_badge.tsx @@ -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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { useUserData } from '../../detections/components/user_info'; +import { hasUserCRUDPermission } from '../../common/utils/privileges'; +import { useKibana } from '../../common/lib/kibana'; +import type { RuleSnoozeSettings } from '../rule_management/logic'; +import { useInvalidateFetchRulesSnoozeSettingsQuery } from '../rule_management/api/hooks/use_fetch_rules_snooze_settings'; + +interface RuleSnoozeBadgeProps { + /** + * Rule's snooze settings, when set to `undefined` considered as a loading state + */ + snoozeSettings: RuleSnoozeSettings | undefined; + /** + * It should represent a user readable error message happened during data snooze settings fetching + */ + error?: string; + showTooltipInline?: boolean; +} + +export function RuleSnoozeBadge({ + snoozeSettings, + error, + showTooltipInline = false, +}: RuleSnoozeBadgeProps): JSX.Element { + const RulesListNotifyBadge = useKibana().services.triggersActionsUi.getRulesListNotifyBadge; + const [{ canUserCRUD }] = useUserData(); + const hasCRUDPermissions = hasUserCRUDPermission(canUserCRUD); + const invalidateFetchRuleSnoozeSettings = useInvalidateFetchRulesSnoozeSettingsQuery(); + const isLoading = !snoozeSettings; + const rule = useMemo(() => { + return { + id: snoozeSettings?.id ?? '', + muteAll: snoozeSettings?.mute_all ?? false, + activeSnoozes: snoozeSettings?.active_snoozes ?? [], + isSnoozedUntil: snoozeSettings?.is_snoozed_until + ? new Date(snoozeSettings.is_snoozed_until) + : undefined, + snoozeSchedule: snoozeSettings?.snooze_schedule, + isEditable: hasCRUDPermissions, + }; + }, [snoozeSettings, hasCRUDPermissions]); + + if (error) { + return ( + + + + ); + } + + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts index af6a82af1a9d5..8b7a9c62b1541 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.test.ts @@ -797,91 +797,6 @@ describe('helpers', () => { meta: { kibana_siem_app_url: 'http://localhost:5601/app/siem', }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for no_actions', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: 'no_actions', - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - actions: [], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for rule', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: 'rule', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - actions: [ - { - group: mockStepData.actions[0].group, - id: mockStepData.actions[0].id, - action_type_id: mockStepData.actions[0].actionTypeId, - params: mockStepData.actions[0].params, - }, - ], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'rule', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for interval', () => { - const mockStepData: ActionsStepRule = { - ...mockData, - throttle: '1d', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result = formatActionsStepData(mockStepData); - const expected: ActionsStepRuleJson = { - actions: [ - { - group: mockStepData.actions[0].group, - id: mockStepData.actions[0].id, - action_type_id: mockStepData.actions[0].actionTypeId, - params: mockStepData.actions[0].params, - }, - ], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: mockStepData.throttle, }; expect(result).toEqual(expected); @@ -913,7 +828,6 @@ describe('helpers', () => { meta: { kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, }, - throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts index 5442727561ce1..e61f55dbce4ec 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_creation/helpers.ts @@ -25,7 +25,6 @@ import type { Type, } from '@kbn/securitysolution-io-ts-alerting-types'; import { ENDPOINT_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; import { assertUnreachable } from '../../../../../common/utility_types'; import { transformAlertToRuleAction, @@ -563,19 +562,12 @@ export const formatAboutStepData = ( }; export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { - const { - actions = [], - responseActions, - enabled, - kibanaSiemAppUrl, - throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, - } = actionsStepData; + const { actions = [], responseActions, enabled, kibanaSiemAppUrl } = actionsStepData; return { actions: actions.map(transformAlertToRuleAction), response_actions: responseActions?.map(transformAlertToRuleResponseAction), enabled, - throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, meta: { kibana_siem_app_url: kibanaSiemAppUrl, }, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.tsx new file mode 100644 index 0000000000000..e610715d676ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/index.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 { useFetchRulesSnoozeSettings } from '../../../../../rule_management/api/hooks/use_fetch_rules_snooze_settings'; +import { RuleSnoozeBadge } from '../../../../../components/rule_snooze_badge'; +import * as i18n from './translations'; + +interface RuleDetailsSnoozeBadge { + /** + * Rule's SO id (not ruleId) + */ + id: string; +} + +export function RuleDetailsSnoozeSettings({ id }: RuleDetailsSnoozeBadge): JSX.Element { + const { data: rulesSnoozeSettings, isFetching, isError } = useFetchRulesSnoozeSettings([id]); + const snoozeSettings = rulesSnoozeSettings?.[0]; + + return ( + + ); +} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts similarity index 81% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts rename to x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts index 1b98a9c6212eb..37b3b6c75ba6e 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management/components/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/components/rule_details_snooze_settings/translations.ts @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; export const UNABLE_TO_FETCH_RULE_SNOOZE_SETTINGS = i18n.translate( - 'xpack.securitySolution.detectionEngine.ruleManagement.ruleSnoozeBadge.error.unableToFetch', + 'xpack.securitySolution.detectionEngine.ruleDetails.rulesSnoozeSettings.error.unableToFetch', { defaultMessage: 'Unable to fetch snooze settings', } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx index 28ed5a658558d..07cbd4294cb22 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.test.tsx @@ -86,6 +86,11 @@ jest.mock('react-router-dom', () => { }; }); +// RuleDetailsSnoozeSettings is an isolated component and not essential for existing tests +jest.mock('./components/rule_details_snooze_settings', () => ({ + RuleDetailsSnoozeSettings: () => <>, +})); + const mockRedirectLegacyUrl = jest.fn(); const mockGetLegacyUrlConflict = jest.fn(); jest.mock('../../../../common/lib/kibana', () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx index e53b23d16a46d..90f1d38f69774 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_details_ui/pages/rule_details/index.tsx @@ -140,6 +140,7 @@ import { EditRuleSettingButtonLink } from '../../../../detections/pages/detectio import { useStartMlJobs } from '../../../rule_management/logic/use_start_ml_jobs'; import { useBulkDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/use_bulk_duplicate_confirmation'; import { BulkActionDuplicateExceptionsConfirmation } from '../../../rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation'; +import { RuleDetailsSnoozeSettings } from './components/rule_details_snooze_settings'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -539,23 +540,30 @@ const RuleDetailsPageComponent: React.FC = ({ const lastExecutionMessage = lastExecution?.message ?? ''; const ruleStatusInfo = useMemo(() => { - return ruleLoading ? ( - - - - ) : ( - - - + return ( + <> + {ruleLoading ? ( + + + + ) : ( + + + + )} + + + + ); - }, [lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); + }, [ruleId, lastExecutionStatus, lastExecutionDate, ruleLoading, isExistingRule, refreshRule]); const ruleError = useMemo(() => { return ruleLoading ? ( @@ -844,7 +852,7 @@ const RuleDetailsPageComponent: React.FC = ({ {ruleId != null && ( { - const ruleSnoozeSettings = rulesSnoozeSettings.data[id]; - - return { - id: ruleSnoozeSettings?.id ?? '', - muteAll: ruleSnoozeSettings?.mute_all ?? false, - activeSnoozes: ruleSnoozeSettings?.active_snoozes ?? [], - isSnoozedUntil: ruleSnoozeSettings?.is_snoozed_until - ? new Date(ruleSnoozeSettings.is_snoozed_until) - : undefined, - snoozeSchedule: ruleSnoozeSettings?.snooze_schedule, - isEditable: hasCRUDPermissions, - }; - }, [id, rulesSnoozeSettings, hasCRUDPermissions]); - - if (rulesSnoozeSettings.isError) { - return ( - - - - ); - } - - return ( - - ); -} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts index 40707a4307f27..487052fcbf2ef 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/__mocks__/mock.ts @@ -200,7 +200,6 @@ export const mockActionsStepRule = (enabled = false): ActionsStepRule => ({ actions: [], kibanaSiemAppUrl: 'http://localhost:5601/app/siem', enabled, - throttle: 'no_actions', }); export const mockDefineStepRule = (): DefineStepRule => ({ diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation.tsx index cd8be2d925c3f..74575c327fbc8 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/bulk_duplicate_exceptions_confirmation.tsx @@ -46,21 +46,35 @@ const BulkActionDuplicateExceptionsConfirmationComponent = ({ defaultFocusedButton="confirm" onCancel={onCancel} > - - {i18n.MODAL_TEXT(rulesCount)}{' '} - - + {i18n.MODAL_TEXT(rulesCount)} + {i18n.DUPLICATE_EXCEPTIONS_INCLUDE_EXPIRED_EXCEPTIONS_LABEL(rulesCount)} + + + ), + 'data-test-subj': DuplicateOptions.withExceptions, + }, + { + id: DuplicateOptions.withExceptionsExcludeExpiredExceptions, + label: ( + + {i18n.DUPLICATE_EXCEPTIONS_TEXT(rulesCount)} + + + ), + 'data-test-subj': DuplicateOptions.withExceptionsExcludeExpiredExceptions, }, { id: DuplicateOptions.withoutExceptions, label: i18n.DUPLICATE_WITHOUT_EXCEPTIONS_TEXT(rulesCount), + 'data-test-subj': DuplicateOptions.withoutExceptions, }, ]} idSelected={selectedDuplicateOption} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx index 8b791ee2aece1..ff41017ac1771 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/forms/rule_actions_form.tsx @@ -23,21 +23,13 @@ import { Field, } from '../../../../../../shared_imports'; import { BulkActionEditType } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import type { - BulkActionEditPayload, - ThrottleForBulkActions, -} from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import { NOTIFICATION_THROTTLE_RULE } from '../../../../../../../common/constants'; +import type { BulkActionEditPayload } from '../../../../../../../common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; import { BulkEditFormWrapper } from './bulk_edit_form_wrapper'; import { bulkAddRuleActions as i18n } from '../translations'; import { useKibana } from '../../../../../../common/lib/kibana'; -import { - ThrottleSelectField, - THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, -} from '../../../../../../detections/components/rules/throttle_select_field'; import { getAllActionMessageParams } from '../../../../../../detections/pages/detection_engine/rules/helpers'; import { RuleActionsField } from '../../../../../../detections/components/rules/rule_actions_field'; @@ -45,19 +37,16 @@ import { debouncedValidateRuleActionsField } from '../../../../../../detections/ const CommonUseField = getUseField({ component: Field }); +type BulkActionsRuleAction = RuleAction & Required>; + export interface RuleActionsFormData { - throttle: ThrottleForBulkActions; - actions: RuleAction[]; + actions: BulkActionsRuleAction[]; overwrite: boolean; } const getFormSchema = ( actionTypeRegistry: ActionTypeRegistryContract ): FormSchema => ({ - throttle: { - label: i18n.THROTTLE_LABEL, - helpText: i18n.THROTTLE_HELP_TEXT, - }, actions: { validations: [ { @@ -75,7 +64,6 @@ const getFormSchema = ( }); const defaultFormData: RuleActionsFormData = { - throttle: NOTIFICATION_THROTTLE_RULE, actions: [], overwrite: false, }; @@ -108,7 +96,7 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction return; } - const { actions = [], throttle: throttleToSubmit, overwrite: overwriteValue } = data; + const { actions = [], overwrite: overwriteValue } = data; const editAction = overwriteValue ? BulkActionEditType.set_rule_actions : BulkActionEditType.add_rule_actions; @@ -117,23 +105,10 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction type: editAction, value: { actions: actions.map(({ actionTypeId, ...action }) => action), - throttle: throttleToSubmit, }, }); }, [form, onConfirm]); - const throttleFieldComponentProps = useMemo( - () => ({ - idAria: 'bulkEditRulesRuleActionThrottle', - 'data-test-subj': 'bulkEditRulesRuleActionThrottle', - hasNoInitialSelection: false, - euiFieldProps: { - options: THROTTLE_OPTIONS_FOR_BULK_RULE_ACTIONS, - }, - }), - [] - ); - const messageVariables = useMemo(() => getAllActionMessageParams(), []); return ( @@ -156,24 +131,11 @@ const RuleActionsFormComponent = ({ rulesCount, onClose, onConfirm }: RuleAction } >
    -
  • - -
  • {i18n.RULE_VARIABLES_DETAIL}
- - - ( ), @@ -147,7 +132,7 @@ export const bulkDuplicateRuleActions = { MODAL_TEXT: (rulesCount: number): JSX.Element => ( ), @@ -155,7 +140,15 @@ export const bulkDuplicateRuleActions = { DUPLICATE_EXCEPTIONS_TEXT: (rulesCount: number) => ( + ), + + DUPLICATE_EXCEPTIONS_INCLUDE_EXPIRED_EXCEPTIONS_LABEL: (rulesCount: number) => ( + ), @@ -163,7 +156,7 @@ export const bulkDuplicateRuleActions = { DUPLICATE_WITHOUT_EXCEPTIONS_TEXT: (rulesCount: number) => ( ), @@ -186,7 +179,7 @@ export const bulkDuplicateRuleActions = { 'xpack.securitySolution.detectionEngine.rules.allRules.bulkActions.duplicate.exceptionsConfirmation.tooltip', { defaultMessage: - ' If you duplicate exceptions, then the shared exceptions list will be duplicated by reference and the default rule exception will be copied and created as a new one', + ' If you choose to duplicate exceptions, the shared exceptions list will be duplicated by reference and the rule exceptions will be copied and created anew', } ), }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx index f9915777483a8..b1804e5c29f0a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/use_bulk_actions.tsx @@ -6,7 +6,6 @@ */ /* eslint-disable complexity */ -import { omit } from 'lodash'; import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiTextColor } from '@elastic/eui'; import type { Toast } from '@kbn/core/public'; @@ -137,7 +136,13 @@ export const useBulkActions = ({ type: BulkActionType.duplicate, duplicatePayload: { include_exceptions: - modalDuplicationConfirmationResult === DuplicateOptions.withExceptions, + modalDuplicationConfirmationResult === DuplicateOptions.withExceptions || + modalDuplicationConfirmationResult === + DuplicateOptions.withExceptionsExcludeExpiredExceptions, + include_expired_exceptions: !( + modalDuplicationConfirmationResult === + DuplicateOptions.withExceptionsExcludeExpiredExceptions + ), }, ...(isAllSelected ? { query: filterQuery } : { ids: selectedRuleIds }), }); @@ -222,16 +227,6 @@ export const useBulkActions = ({ return; } - // TODO: https://github.com/elastic/kibana/issues/148414 - // Strip frequency from actions to comply with Security Solution alert API - if ('actions' in editPayload.value) { - // `actions.frequency` is included in the payload from TriggersActionsUI ActionForm - // but is not included in the type definition for the editPayload, because this type - // definition comes from the Security Solution alert API - // TODO https://github.com/elastic/kibana/issues/148414 fix this discrepancy - editPayload.value.actions = editPayload.value.actions.map((a) => omit(a, 'frequency')); - } - startTransaction({ name: BULK_RULE_ACTIONS.EDIT }); const hideWarningToast = () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts index c8f49ebe4a6c6..f25b1331d766a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/bulk_actions/utils/compute_dry_run_edit_payload.ts @@ -50,7 +50,7 @@ export function computeDryRunEditPayload(editAction: BulkActionEditType): BulkAc return [ { type: editAction, - value: { throttle: '1h', actions: [] }, + value: { actions: [] }, }, ]; case BulkActionEditType.set_schedule: diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx index 8ab84b2e60a60..9ee763899eab7 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/__mocks__/rules_table_context.tsx @@ -15,6 +15,7 @@ export const useRulesTableContextMock = { rulesSnoozeSettings: { data: {}, isLoading: false, + isFetching: false, isError: false, }, pagination: { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx index fa2c64f01d2d4..938174d0c567d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table/rules_table_context.tsx @@ -40,8 +40,18 @@ import { RuleSource } from './rules_table_saved_state'; import { useRulesTableSavedState } from './use_rules_table_saved_state'; interface RulesSnoozeSettings { - data: Record; // The key is a rule SO's id (not ruleId) + /** + * A map object using rule SO's id (not ruleId) as keys and snooze settings as values + */ + data: Record; + /** + * Sets to true during the first data loading + */ isLoading: boolean; + /** + * Sets to true during data loading + */ + isFetching: boolean; isError: boolean; } @@ -290,6 +300,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide const { data: rulesSnoozeSettings, isLoading: isSnoozeSettingsLoading, + isFetching: isSnoozeSettingsFetching, isError: isSnoozeSettingsFetchError, refetch: refetchSnoozeSettings, } = useFetchRulesSnoozeSettings( @@ -349,6 +360,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide rulesSnoozeSettings: { data: rulesSnoozeSettingsMap, isLoading: isSnoozeSettingsLoading, + isFetching: isSnoozeSettingsFetching, isError: isSnoozeSettingsFetchError, }, pagination: { @@ -382,6 +394,7 @@ export const RulesTableContextProvider = ({ children }: RulesTableContextProvide rules, rulesSnoozeSettings, isSnoozeSettingsLoading, + isSnoozeSettingsFetching, isSnoozeSettingsFetchError, page, perPage, diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts index 52b4a5d4ba622..ad3cd89604030 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/translations.ts @@ -21,3 +21,10 @@ export const ML_RULE_JOBS_WARNING_BUTTON_LABEL = i18n.translate( defaultMessage: 'Visit rule details page to investigate', } ); + +export const UNABLE_TO_FETCH_RULES_SNOOZE_SETTINGS = i18n.translate( + 'xpack.securitySolution.detectionEngine.ruleManagement.rulesSnoozeSettings.error.unableToFetch', + { + defaultMessage: 'Unable to fetch snooze settings', + } +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx index e20a2f2c70e4f..0ffb0ac7574a6 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_columns.tsx @@ -22,7 +22,7 @@ import type { } from '../../../../../common/detection_engine/rule_monitoring'; import { isMlRule } from '../../../../../common/machine_learning/helpers'; import { getEmptyTagValue } from '../../../../common/components/empty_value'; -import { RuleSnoozeBadge } from '../../../rule_management/components/rule_snooze_badge'; +import { RuleSnoozeBadge } from '../../../components/rule_snooze_badge'; import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; import { SecuritySolutionLinkAnchor } from '../../../../common/components/links'; import { getRuleDetailsTabUrl } from '../../../../common/components/link_to/redirect_to_detection_engine'; @@ -46,6 +46,7 @@ import { useHasActionsPrivileges } from './use_has_actions_privileges'; import { useHasMlPermissions } from './use_has_ml_permissions'; import { useRulesTableActions } from './use_rules_table_actions'; import { MlRuleWarningPopover } from './ml_rule_warning_popover'; +import * as rulesTableI18n from './translations'; export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; @@ -108,15 +109,33 @@ const useEnabledColumn = ({ hasCRUDPermissions, startMlJobs }: ColumnsProps): Ta }; const useRuleSnoozeColumn = (): TableColumn => { + const { + state: { rulesSnoozeSettings }, + } = useRulesTableContext(); + return useMemo( () => ({ field: 'snooze', name: i18n.COLUMN_SNOOZE, - render: (_, rule: Rule) => , + render: (_, rule: Rule) => { + const snoozeSettings = rulesSnoozeSettings.data[rule.id]; + const { isFetching, isError } = rulesSnoozeSettings; + + return ( + + ); + }, width: '100px', sortable: false, }), - [] + [rulesSnoozeSettings] ); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx index 62af07af3ea24..b5999b926b97f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/use_rules_table_actions.tsx @@ -77,7 +77,13 @@ export const useRulesTableActions = ({ ids: [rule.id], duplicatePayload: { include_exceptions: - modalDuplicationConfirmationResult === DuplicateOptions.withExceptions, + modalDuplicationConfirmationResult === DuplicateOptions.withExceptions || + modalDuplicationConfirmationResult === + DuplicateOptions.withExceptionsExcludeExpiredExceptions, + include_expired_exceptions: !( + modalDuplicationConfirmationResult === + DuplicateOptions.withExceptionsExcludeExpiredExceptions + ), }, }); const createdRules = result?.attributes.results.created; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx index 9d7812b833b46..f68f8d54a5b19 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_by_type_panel/columns.tsx @@ -56,7 +56,7 @@ export const getAlertsTypeTableColumns = ( { }); }); - it('invokes useGlobalTime() with false to prevent global queries from being deleted when the component unmounts', async () => { - await act(async () => { - mount( - - - - ); - - expect(useGlobalTime).toBeCalledWith(false); - }); - }); - it('renders with the specified `alignHeader` alignment', async () => { await act(async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 26eb4522ed617..f9967a44ffb6e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -84,7 +84,7 @@ export const AlertsCountPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const isChartEmbeddablesEnabled = useIsExperimentalFeatureEnabled('chartEmbeddablesEnabled'); const isAlertsPageChartsEnabled = useIsExperimentalFeatureEnabled('alertsPageChartsEnabled'); // create a unique, but stable (across re-renders) query id diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index d1cb85cdf4564..e1945ca151cd8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -152,7 +152,7 @@ export const AlertsHistogramPanel = memo( isExpanded, setIsExpanded, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); // create a unique, but stable (across re-renders) query id const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuidv4()}`, []); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx index fb5024d3c2e50..e8d0ddd061e81 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_summary_charts_panel/use_summary_chart_data.tsx @@ -82,7 +82,7 @@ export const useSummaryChartData: UseAlerts = ({ signalIndexName, skip = false, }) => { - const { to, from, deleteQuery, setQuery } = useGlobalTime(false); + const { to, from, deleteQuery, setQuery } = useGlobalTime(); const [updatedAt, setUpdatedAt] = useState(Date.now()); const [items, setItems] = useState([]); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx new file mode 100644 index 0000000000000..4a57d7cac8e73 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.test.tsx @@ -0,0 +1,376 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, render, within } from '@testing-library/react'; +import type { Filter } from '@kbn/es-query'; +import useResizeObserver from 'use-resize-observer/polyfilled'; + +import '../../../common/mock/match_media'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../../common/mock'; +import type { AlertsTableComponentProps } from './alerts_grouping'; +import { GroupedAlertsTable } from './alerts_grouping'; +import { TableId } from '@kbn/securitysolution-data-table'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; +import { createStore } from '../../../common/store'; +import { useKibana as mockUseKibana } from '../../../common/lib/kibana/__mocks__'; +import { createTelemetryServiceMock } from '../../../common/lib/telemetry/telemetry_service.mock'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { groupingSearchResponse } from './grouping_settings/mock'; + +jest.mock('../../containers/detection_engine/alerts/use_query'); +jest.mock('../../../common/containers/sourcerer'); +jest.mock('../../../common/utils/normalize_time_range'); +jest.mock('../../../common/containers/use_global_time', () => ({ + useGlobalTime: jest.fn().mockReturnValue({ + from: '2020-07-07T08:20:18.966Z', + isInitializing: false, + to: '2020-07-08T08:20:18.966Z', + setQuery: jest.fn(), + }), +})); + +const mockOptions = [ + { label: 'ruleName', key: 'kibana.alert.rule.name' }, + { label: 'userName', key: 'user.name' }, + { label: 'hostName', key: 'host.name' }, + { label: 'sourceIP', key: 'source.ip' }, +]; +// +jest.mock('./grouping_settings', () => { + const actual = jest.requireActual('./grouping_settings'); + + return { + ...actual, + getDefaultGroupingOptions: () => mockOptions, + }; +}); + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const mockUseFieldBrowserOptions = jest.fn(); +jest.mock('../../../timelines/components/fields_browser', () => ({ + useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), +})); + +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer/polyfilled'); +mockUseResizeObserver.mockImplementation(() => ({})); +const mockedUseKibana = mockUseKibana(); +const mockedTelemetry = createTelemetryServiceMock(); +jest.mock('../../../common/lib/kibana', () => { + const original = jest.requireActual('../../../common/lib/kibana'); + + return { + ...original, + useKibana: () => ({ + ...mockedUseKibana, + services: { + ...mockedUseKibana.services, + telemetry: mockedTelemetry, + }, + }), + }; +}); + +jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ + useAddBulkToTimelineAction: jest.fn(() => {}), +})); +const sourcererDataView = { + indicesExist: true, + loading: false, + indexPattern: { + fields: [], + }, + browserFields: {}, +}; +const renderChildComponent = (groupingFilters: Filter[]) =>

; + +const testProps: AlertsTableComponentProps = { + defaultFilters: [], + from: '2020-07-07T08:20:18.966Z', + globalFilters: [], + globalQuery: { + query: 'query', + language: 'language', + }, + hasIndexMaintenance: true, + hasIndexWrite: true, + loading: false, + renderChildComponent, + runtimeMappings: {}, + signalIndexName: 'test', + tableId: TableId.test, + to: '2020-07-08T08:20:18.966Z', +}; + +const mockUseQueryAlerts = useQueryAlerts as jest.Mock; +const mockQueryResponse = { + loading: false, + data: {}, + setQuery: () => {}, + response: '', + request: '', + refetch: () => {}, +}; + +const getMockStorageState = (groups: string[] = ['none']) => + JSON.stringify({ + [testProps.tableId]: { + activeGroups: groups, + options: mockOptions, + }, + }); + +describe('GroupedAlertsTable', () => { + const { storage } = createSecuritySolutionStorageMock(); + let store: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + (useSourcererDataView as jest.Mock).mockReturnValue({ + ...sourcererDataView, + selectedPatterns: ['myFakebeat-*'], + }); + mockUseQueryAlerts.mockImplementation((i) => { + if (i.skip) { + return mockQueryResponse; + } + if (i.query.aggs.groupByFields.multi_terms != null) { + return { + ...mockQueryResponse, + data: groupingSearchResponse.ruleName, + }; + } + return { + ...mockQueryResponse, + data: i.query.aggs.groupByFields.terms.field != null ? groupingSearchResponse.hostName : {}, + }; + }); + }); + + it('calls the proper initial dispatch actions for groups', () => { + const { getByTestId, queryByTestId } = render( + + + + ); + + expect(queryByTestId('empty-results-panel')).not.toBeInTheDocument(); + expect(queryByTestId('group-selector-dropdown')).not.toBeInTheDocument(); + expect(getByTestId('alerts-table')).toBeInTheDocument(); + expect(mockDispatch).toHaveBeenCalledTimes(1); + expect(mockDispatch.mock.calls[0][0].type).toEqual( + 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' + ); + }); + + it('renders empty grouping table when group is selected without data', async () => { + mockUseQueryAlerts.mockReturnValue(mockQueryResponse); + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + const { getByTestId, queryByTestId } = render( + + + + ); + expect(queryByTestId('alerts-table')).not.toBeInTheDocument(); + expect(getByTestId('empty-results-panel')).toBeInTheDocument(); + }); + + it('renders grouping table in first accordion level when single group is selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('renders grouping table in second accordion level when 2 groups are selected', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name'])); + + const { getAllByTestId } = render( + + + + ); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + expect(within(level0).queryByTestId('alerts-table')).not.toBeInTheDocument(); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + const level1 = within(getAllByTestId('grouping-accordion-content')[1]); + expect(level1.getByTestId('alerts-table')).toBeInTheDocument(); + }); + + it('resets all levels pagination when selected group changes', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + fireEvent.click(getAllByTestId('group-selector-dropdown')[0]); + fireEvent.click(getAllByTestId('panel-user.name')[0]); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + // level 2 has been removed with the group selection change + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets all levels pagination when global query updates', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId, rerender } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + rerender( + + + + ); + + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + getByTestId('grouping-level-2-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual('true'); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual(null); + }); + }); + + it('resets only most inner group pagination when its parent groups open/close', async () => { + jest + .spyOn(window.localStorage, 'getItem') + .mockReturnValue(getMockStorageState(['kibana.alert.rule.name', 'host.name', 'user.name'])); + + const { getByTestId, getAllByTestId } = render( + + + + ); + + fireEvent.click(getByTestId('pagination-button-1')); + fireEvent.click(getAllByTestId('group-panel-toggle')[0]); + + const level0 = getAllByTestId('grouping-accordion-content')[0]; + fireEvent.click(within(level0).getByTestId('pagination-button-1')); + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[0]); + + const level1 = getAllByTestId('grouping-accordion-content')[1]; + fireEvent.click(within(level1).getByTestId('pagination-button-1')); + + fireEvent.click(within(level0).getAllByTestId('group-panel-toggle')[28]); + [ + getByTestId('grouping-level-0-pagination'), + getByTestId('grouping-level-1-pagination'), + ].forEach((pagination) => { + expect( + within(pagination).getByTestId('pagination-button-0').getAttribute('aria-current') + ).toEqual(null); + expect( + within(pagination).getByTestId('pagination-button-1').getAttribute('aria-current') + ).toEqual('true'); + }); + + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-0') + .getAttribute('aria-current') + ).toEqual('true'); + expect( + within(getByTestId('grouping-level-2-pagination')) + .getByTestId('pagination-button-1') + .getAttribute('aria-current') + ).toEqual(null); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx index e5868970f6768..99ae3f4ff3433 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_grouping.tsx @@ -5,50 +5,29 @@ * 2.0. */ -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; -import { useDispatch } from 'react-redux'; -import { v4 as uuidv4 } from 'uuid'; +import { useDispatch, useSelector } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; -import { buildEsQuery } from '@kbn/es-query'; -import { getEsQueryConfig } from '@kbn/data-plugin/common'; -import type { - GroupingFieldTotalAggregation, - GroupingAggregation, -} from '@kbn/securitysolution-grouping'; -import { useGrouping, isNoneGroup } from '@kbn/securitysolution-grouping'; +import type { GroupOption } from '@kbn/securitysolution-grouping'; +import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; +import { isEmpty, isEqual } from 'lodash/fp'; +import type { Storage } from '@kbn/kibana-utils-plugin/public'; import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; -import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import { groupSelectors } from '../../../common/store/grouping'; +import type { State } from '../../../common/store'; +import { updateGroupSelector } from '../../../common/store/grouping/actions'; import type { Status } from '../../../../common/detection_engine/schemas/common'; -import { InspectButton } from '../../../common/components/inspect'; import { defaultUnit } from '../../../common/components/toolbar/unit'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; -import { combineQueries } from '../../../common/lib/kuery'; import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; -import { useKibana } from '../../../common/lib/kibana'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; -import { useInspectButton } from '../alerts_kpis/common/hooks'; - -import { buildTimeRangeFilter } from './helpers'; -import * as i18n from './translations'; -import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; -import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; -import { - getAlertsGroupingQuery, - getDefaultGroupingOptions, - renderGroupPanel, - getStats, - useGroupTakeActionsItems, -} from './grouping_settings'; -import { updateGroupSelector, updateSelectedGroup } from '../../../common/store/grouping/actions'; +import { getDefaultGroupingOptions, renderGroupPanel, getStats } from './grouping_settings'; +import { useKibana } from '../../../common/lib/kibana'; +import { GroupedSubLevel } from './alerts_sub_grouping'; import { track } from '../../../common/lib/telemetry'; -const ALERTS_GROUPING_ID = 'alerts-grouping'; - export interface AlertsTableComponentProps { - currentAlertStatusFilterValue?: Status; + currentAlertStatusFilterValue?: Status[]; defaultFilters?: Filter[]; from: string; globalFilters: Filter[]; @@ -63,52 +42,37 @@ export interface AlertsTableComponentProps { to: string; } -export const GroupedAlertsTableComponent: React.FC = ({ - defaultFilters = [], - from, - globalFilters, - globalQuery, - hasIndexMaintenance, - hasIndexWrite, - loading, - tableId, - to, - runtimeMappings, - signalIndexName, - currentAlertStatusFilterValue, - renderChildComponent, -}) => { - const dispatch = useDispatch(); +const DEFAULT_PAGE_SIZE = 25; +const DEFAULT_PAGE_INDEX = 0; +const MAX_GROUPING_LEVELS = 3; - const { browserFields, indexPattern, selectedPatterns } = useSourcererDataView( - SourcererScopeName.detections +const useStorage = (storage: Storage, tableId: string) => + useMemo( + () => ({ + getStoragePageSize: (): number[] => { + const pageSizes = storage.get(`grouping-table-${tableId}`); + if (!pageSizes) { + return Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_SIZE); + } + return pageSizes; + }, + setStoragePageSize: (pageSizes: number[]) => { + storage.set(`grouping-table-${tableId}`, pageSizes); + }, + }), + [storage, tableId] ); + +const GroupedAlertsTableComponent: React.FC = (props) => { + const dispatch = useDispatch(); + + const { indexPattern, selectedPatterns } = useSourcererDataView(SourcererScopeName.detections); + const { - services: { uiSettings, telemetry }, + services: { storage, telemetry }, } = useKibana(); - const getGlobalQuery = useCallback( - (customFilters: Filter[]) => { - if (browserFields != null && indexPattern != null) { - return combineQueries({ - config: getEsQueryConfig(uiSettings), - dataProviders: [], - indexPattern, - browserFields, - filters: [ - ...(defaultFilters ?? []), - ...globalFilters, - ...customFilters, - ...buildTimeRangeFilter(from, to), - ], - kqlQuery: globalQuery, - kqlMode: globalQuery.language, - }); - } - return null; - }, - [browserFields, indexPattern, uiSettings, defaultFilters, globalFilters, from, to, globalQuery] - ); + const { getStoragePageSize, setStoragePageSize } = useStorage(storage, props.tableId); const { onGroupChange, onGroupToggle } = useMemo( () => ({ @@ -125,153 +89,146 @@ export const GroupedAlertsTableComponent: React.FC = [telemetry] ); - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); - - const inspect = useMemo( - () => ( - - ), - [uniqueQueryId] - ); - - const { groupSelector, getGrouping, selectedGroup, pagination } = useGrouping({ + const { groupSelector, getGrouping, selectedGroups } = useGrouping({ componentProps: { groupPanelRenderer: renderGroupPanel, groupStatsRenderer: getStats, - inspectButton: inspect, onGroupToggle, - renderChildComponent, unit: defaultUnit, }, - defaultGroupingOptions: getDefaultGroupingOptions(tableId), + defaultGroupingOptions: getDefaultGroupingOptions(props.tableId), fields: indexPattern.fields, - groupingId: tableId, + groupingId: props.tableId, + maxGroupingLevels: MAX_GROUPING_LEVELS, onGroupChange, tracker: track, }); - const resetPagination = pagination.reset; - useEffect(() => { - dispatch(updateGroupSelector({ groupSelector })); - }, [dispatch, groupSelector]); + const getGroupSelector = groupSelectors.getGroupSelector(); - useEffect(() => { - dispatch(updateSelectedGroup({ selectedGroup })); - }, [dispatch, selectedGroup]); - - useInvalidFilterQuery({ - id: tableId, - filterQuery: getGlobalQuery([])?.filterQuery, - kqlError: getGlobalQuery([])?.kqlError, - query: globalQuery, - startDate: from, - endDate: to, - }); + const groupSelectorInRedux = useSelector((state: State) => getGroupSelector(state)); + const selectorOptions = useRef([]); - const { deleteQuery, setQuery } = useGlobalTime(false); - const additionalFilters = useMemo(() => { - resetPagination(); - try { - return [ - buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ - ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), - ...(defaultFilters ?? []), - ]), - ]; - } catch (e) { - return []; + useEffect(() => { + if ( + isNoneGroup(selectedGroups) && + groupSelector.props.options.length > 0 && + (groupSelectorInRedux == null || + !isEqual(selectorOptions.current, groupSelector.props.options)) + ) { + selectorOptions.current = groupSelector.props.options; + dispatch(updateGroupSelector({ groupSelector })); + } else if (!isNoneGroup(selectedGroups) && groupSelectorInRedux !== null) { + dispatch(updateGroupSelector({ groupSelector: null })); } - }, [defaultFilters, globalFilters, globalQuery, resetPagination]); + }, [dispatch, groupSelector, groupSelectorInRedux, selectedGroups]); - const queryGroups = useMemo( - () => - getAlertsGroupingQuery({ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pageSize: pagination.pageSize, - pageIndex: pagination.pageIndex, - }), - [ - additionalFilters, - selectedGroup, - from, - runtimeMappings, - to, - pagination.pageSize, - pagination.pageIndex, - ] + const [pageIndex, setPageIndex] = useState( + Array(MAX_GROUPING_LEVELS).fill(DEFAULT_PAGE_INDEX) ); + const [pageSize, setPageSize] = useState(getStoragePageSize); - const { - data: alertsGroupsData, - loading: isLoadingGroups, - refetch, - request, - response, - setQuery: setAlertsQuery, - } = useQueryAlerts< - {}, - GroupingAggregation & - GroupingFieldTotalAggregation - >({ - query: queryGroups, - indexName: signalIndexName, - queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, - skip: isNoneGroup(selectedGroup), - }); + const resetAllPagination = useCallback(() => { + setPageIndex((curr) => curr.map(() => DEFAULT_PAGE_INDEX)); + }, []); useEffect(() => { - if (!isNoneGroup(selectedGroup)) { - setAlertsQuery(queryGroups); - } - }, [queryGroups, selectedGroup, setAlertsQuery]); + resetAllPagination(); + }, [resetAllPagination, selectedGroups]); + + const setPageVar = useCallback( + (newNumber: number, groupingLevel: number, pageType: 'index' | 'size') => { + if (pageType === 'index') { + setPageIndex((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + return newArr; + }); + } - useInspectButton({ - deleteQuery, - loading: isLoadingGroups, - response, - setQuery, - refetch, - request, - uniqueQueryId, - }); + if (pageType === 'size') { + setPageSize((currentIndex) => { + const newArr = [...currentIndex]; + newArr[groupingLevel] = newNumber; + setStoragePageSize(newArr); + return newArr; + }); + } + }, + [setStoragePageSize] + ); - const takeActionItems = useGroupTakeActionsItems({ - indexName: indexPattern.title, - currentStatus: currentAlertStatusFilterValue, - showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + const nonGroupingFilters = useRef({ + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, }); - const getTakeActionItems = useCallback( - (groupFilters: Filter[], groupNumber: number) => - takeActionItems({ - query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, - tableId, - groupNumber, - selectedGroup, - }), - [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] - ); + useEffect(() => { + const nonGrouping = { + defaultFilters: props.defaultFilters, + globalFilters: props.globalFilters, + globalQuery: props.globalQuery, + }; + if (!isEqual(nonGroupingFilters.current, nonGrouping)) { + resetAllPagination(); + nonGroupingFilters.current = nonGrouping; + } + }, [props.defaultFilters, props.globalFilters, props.globalQuery, resetAllPagination]); + + const getLevel = useCallback( + (level: number, selectedGroup: string, parentGroupingFilter?: string) => { + let rcc; + if (level < selectedGroups.length - 1) { + rcc = (groupingFilters: Filter[]) => { + return getLevel( + level + 1, + selectedGroups[level + 1], + JSON.stringify([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]) + ); + }; + } else { + rcc = (groupingFilters: Filter[]) => { + return props.renderChildComponent([ + ...groupingFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]); + }; + } - const groupedAlerts = useMemo( - () => - getGrouping({ - data: alertsGroupsData?.aggregations, - isLoading: loading || isLoadingGroups, - takeActionItems: getTakeActionItems, - }), - [alertsGroupsData?.aggregations, getGrouping, getTakeActionItems, isLoadingGroups, loading] + const resetGroupChildrenPagination = (parentLevel: number) => { + setPageIndex((allPages) => { + const resetPages = allPages.splice(parentLevel + 1, allPages.length); + return [...allPages, ...resetPages.map(() => DEFAULT_PAGE_INDEX)]; + }); + }; + return ( + resetGroupChildrenPagination(level)} + pageIndex={pageIndex[level] ?? DEFAULT_PAGE_INDEX} + pageSize={pageSize[level] ?? DEFAULT_PAGE_SIZE} + parentGroupingFilter={parentGroupingFilter} + renderChildComponent={rcc} + selectedGroup={selectedGroup} + setPageIndex={(newIndex: number) => setPageVar(newIndex, level, 'index')} + setPageSize={(newSize: number) => setPageVar(newSize, level, 'size')} + /> + ); + }, + [getGrouping, pageIndex, pageSize, props, selectedGroups, setPageVar] ); if (isEmpty(selectedPatterns)) { return null; } - return groupedAlerts; + return getLevel(0, selectedGroups[0]); }; export const GroupedAlertsTable = React.memo(GroupedAlertsTableComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx new file mode 100644 index 0000000000000..cf8a8128cb166 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/alerts_sub_grouping.tsx @@ -0,0 +1,259 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, useMemo } from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import type { Filter, Query } from '@kbn/es-query'; +import { buildEsQuery } from '@kbn/es-query'; +import type { GroupingAggregation } from '@kbn/securitysolution-grouping'; +import { isNoneGroup } from '@kbn/securitysolution-grouping'; +import { getEsQueryConfig } from '@kbn/data-plugin/common'; +import type { DynamicGroupingProps } from '@kbn/securitysolution-grouping/src'; +import type { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/types'; +import type { TableIdLiteral } from '@kbn/securitysolution-data-table'; +import { combineQueries } from '../../../common/lib/kuery'; +import { SourcererScopeName } from '../../../common/store/sourcerer/model'; +import type { AlertsGroupingAggregation } from './grouping_settings/types'; +import type { Status } from '../../../../common/detection_engine/schemas/common'; +import { InspectButton } from '../../../common/components/inspect'; +import { useSourcererDataView } from '../../../common/containers/sourcerer'; +import { useKibana } from '../../../common/lib/kibana'; +import { useGlobalTime } from '../../../common/containers/use_global_time'; +import { useInvalidFilterQuery } from '../../../common/hooks/use_invalid_filter_query'; +import { useInspectButton } from '../alerts_kpis/common/hooks'; +import { buildTimeRangeFilter } from './helpers'; + +import * as i18n from './translations'; +import { useQueryAlerts } from '../../containers/detection_engine/alerts/use_query'; +import { ALERTS_QUERY_NAMES } from '../../containers/detection_engine/alerts/constants'; +import { getAlertsGroupingQuery, useGroupTakeActionsItems } from './grouping_settings'; + +const ALERTS_GROUPING_ID = 'alerts-grouping'; + +interface OwnProps { + currentAlertStatusFilterValue?: Status[]; + defaultFilters?: Filter[]; + from: string; + getGrouping: ( + props: Omit, 'groupSelector' | 'pagination'> + ) => React.ReactElement; + globalFilters: Filter[]; + globalQuery: Query; + groupingLevel?: number; + hasIndexMaintenance: boolean; + hasIndexWrite: boolean; + loading: boolean; + onGroupClose: () => void; + pageIndex: number; + pageSize: number; + parentGroupingFilter?: string; + renderChildComponent: (groupingFilters: Filter[]) => React.ReactElement; + runtimeMappings: MappingRuntimeFields; + selectedGroup: string; + setPageIndex: (newIndex: number) => void; + setPageSize: (newSize: number) => void; + signalIndexName: string | null; + tableId: TableIdLiteral; + to: string; +} + +export type AlertsTableComponentProps = OwnProps; + +export const GroupedSubLevelComponent: React.FC = ({ + currentAlertStatusFilterValue, + defaultFilters = [], + from, + getGrouping, + globalFilters, + globalQuery, + groupingLevel, + hasIndexMaintenance, + hasIndexWrite, + loading, + onGroupClose, + pageIndex, + pageSize, + parentGroupingFilter, + renderChildComponent, + runtimeMappings, + selectedGroup, + setPageIndex, + setPageSize, + signalIndexName, + tableId, + to, +}) => { + const { + services: { uiSettings }, + } = useKibana(); + const { browserFields, indexPattern } = useSourcererDataView(SourcererScopeName.detections); + + const getGlobalQuery = useCallback( + (customFilters: Filter[]) => { + if (browserFields != null && indexPattern != null) { + return combineQueries({ + config: getEsQueryConfig(uiSettings), + dataProviders: [], + indexPattern, + browserFields, + filters: [ + ...(defaultFilters ?? []), + ...globalFilters, + ...customFilters, + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ...buildTimeRangeFilter(from, to), + ], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + }); + } + return null; + }, + [ + browserFields, + defaultFilters, + from, + globalFilters, + globalQuery, + indexPattern, + parentGroupingFilter, + to, + uiSettings, + ] + ); + + const additionalFilters = useMemo(() => { + try { + return [ + buildEsQuery(undefined, globalQuery != null ? [globalQuery] : [], [ + ...(globalFilters?.filter((f) => f.meta.disabled === false) ?? []), + ...(defaultFilters ?? []), + ...(parentGroupingFilter ? JSON.parse(parentGroupingFilter) : []), + ]), + ]; + } catch (e) { + return []; + } + }, [defaultFilters, globalFilters, globalQuery, parentGroupingFilter]); + + const queryGroups = useMemo(() => { + return getAlertsGroupingQuery({ + additionalFilters, + selectedGroup, + from, + runtimeMappings, + to, + pageSize, + pageIndex, + }); + }, [additionalFilters, from, pageIndex, pageSize, runtimeMappings, selectedGroup, to]); + + const emptyGlobalQuery = useMemo(() => getGlobalQuery([]), [getGlobalQuery]); + + useInvalidFilterQuery({ + id: tableId, + filterQuery: emptyGlobalQuery?.filterQuery, + kqlError: emptyGlobalQuery?.kqlError, + query: globalQuery, + startDate: from, + endDate: to, + }); + + const { + data: alertsGroupsData, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery: setAlertsQuery, + } = useQueryAlerts<{}, GroupingAggregation>({ + query: queryGroups, + indexName: signalIndexName, + queryName: ALERTS_QUERY_NAMES.ALERTS_GROUPING, + skip: isNoneGroup([selectedGroup]), + }); + + useEffect(() => { + if (!isNoneGroup([selectedGroup])) { + setAlertsQuery(queryGroups); + } + }, [queryGroups, selectedGroup, setAlertsQuery]); + + const { deleteQuery, setQuery } = useGlobalTime(); + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ALERTS_GROUPING_ID}-${uuidv4()}`, []); + + useInspectButton({ + deleteQuery, + loading: isLoadingGroups, + refetch, + request, + response, + setQuery, + uniqueQueryId, + }); + + const inspect = useMemo( + () => ( + + ), + [uniqueQueryId] + ); + + const takeActionItems = useGroupTakeActionsItems({ + indexName: indexPattern.title, + currentStatus: currentAlertStatusFilterValue, + showAlertStatusActions: hasIndexWrite && hasIndexMaintenance, + }); + + const getTakeActionItems = useCallback( + (groupFilters: Filter[], groupNumber: number) => + takeActionItems({ + groupNumber, + query: getGlobalQuery([...(defaultFilters ?? []), ...groupFilters])?.filterQuery, + selectedGroup, + tableId, + }), + [defaultFilters, getGlobalQuery, selectedGroup, tableId, takeActionItems] + ); + + return useMemo( + () => + getGrouping({ + activePage: pageIndex, + data: alertsGroupsData?.aggregations, + groupingLevel, + inspectButton: inspect, + isLoading: loading || isLoadingGroups, + itemsPerPage: pageSize, + onChangeGroupsItemsPerPage: (size: number) => setPageSize(size), + onChangeGroupsPage: (index) => setPageIndex(index), + renderChildComponent, + onGroupClose, + selectedGroup, + takeActionItems: getTakeActionItems, + }), + [ + alertsGroupsData?.aggregations, + getGrouping, + getTakeActionItems, + groupingLevel, + inspect, + isLoadingGroups, + loading, + pageIndex, + pageSize, + renderChildComponent, + onGroupClose, + selectedGroup, + setPageIndex, + setPageSize, + ] + ); +}; + +export const GroupedSubLevel = React.memo(GroupedSubLevelComponent); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx index f84305dcb3b37..d663b2abc2c61 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.test.tsx @@ -30,7 +30,7 @@ describe('useGroupTakeActionsItems', () => { groupNumber: 0, selectedGroup: 'test', }; - it('returns array take actions items available for alerts table if showAlertStatusActions is true', async () => { + it('returns all take actions items if showAlertStatusActions is true and currentStatus is undefined', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -47,7 +47,106 @@ describe('useGroupTakeActionsItems', () => { }); }); - it('returns empty array of take actions items available for alerts table if showAlertStatusActions is false', async () => { + it('returns all take actions items if currentStatus is []', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: [], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns all take actions items if currentStatus.length > 1', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open', 'closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); + + it('returns acknowledged & closed take actions items if currentStatus === ["open"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['open'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('acknowledge'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns open & acknowledged take actions items if currentStatus === ["closed"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['closed'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('acknowledge'); + }); + }); + + it('returns open & closed take actions items if currentStatus === ["acknowledged"]', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + currentStatus: ['acknowledged'], + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + const currentParams = result.current(getActionItemsParams); + expect(currentParams.length).toEqual(2); + expect(currentParams[0].key).toEqual('open'); + expect(currentParams[1].key).toEqual('close'); + }); + }); + + it('returns empty take actions items if showAlertStatusActions is false', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook( () => @@ -63,4 +162,20 @@ describe('useGroupTakeActionsItems', () => { expect(result.current(getActionItemsParams).length).toEqual(0); }); }); + it('returns array take actions items if showAlertStatusActions is true', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + useGroupTakeActionsItems({ + indexName: '.alerts-security.alerts-default', + showAlertStatusActions: true, + }), + { + wrapper: wrapperContainer, + } + ); + await waitForNextUpdate(); + expect(result.current(getActionItemsParams).length).toEqual(3); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx index d2baadb99d124..5d151d2e4cc88 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/group_take_action_items.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import type { Status } from '../../../../../common/detection_engine/schemas/common'; @@ -30,8 +30,9 @@ import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; import * as i18n from '../translations'; import { getTelemetryEvent, METRIC_TYPE, track } from '../../../../common/lib/telemetry'; import type { StartServices } from '../../../../types'; + export interface TakeActionsProps { - currentStatus?: Status; + currentStatus?: Status[]; indexName: string; showAlertStatusActions?: boolean; } @@ -182,7 +183,7 @@ export const useGroupTakeActionsItems = ({ ] ); - const items = useMemo(() => { + return useMemo(() => { const getActionItems = ({ query, tableId, @@ -196,61 +197,89 @@ export const useGroupTakeActionsItems = ({ }) => { const actionItems: JSX.Element[] = []; if (showAlertStatusActions) { - if (currentStatus !== FILTER_OPEN) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_OPEN as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_OPEN_SELECTED} - - ); - } - if (currentStatus !== FILTER_ACKNOWLEDGED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_ACKNOWLEDGED_SELECTED} - - ); - } - if (currentStatus !== FILTER_CLOSED) { - actionItems.push( - - onClickUpdate({ - groupNumber, - query, - selectedGroup, - status: FILTER_CLOSED as AlertWorkflowStatus, - tableId, - }) - } - > - {BULK_ACTION_CLOSE_SELECTED} - + if (currentStatus && currentStatus.length === 1) { + const singleStatus = currentStatus[0]; + if (singleStatus !== FILTER_OPEN) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_OPEN as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_OPEN_SELECTED} + + ); + } + if (singleStatus !== FILTER_ACKNOWLEDGED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_ACKNOWLEDGED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_ACKNOWLEDGED_SELECTED} + + ); + } + if (singleStatus !== FILTER_CLOSED) { + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: FILTER_CLOSED as AlertWorkflowStatus, + tableId, + }) + } + > + {BULK_ACTION_CLOSE_SELECTED} + + ); + } + } else { + const statusArr = { + [FILTER_OPEN]: BULK_ACTION_OPEN_SELECTED, + [FILTER_ACKNOWLEDGED]: BULK_ACTION_ACKNOWLEDGED_SELECTED, + [FILTER_CLOSED]: BULK_ACTION_CLOSE_SELECTED, + }; + Object.keys(statusArr).forEach((workflowStatus) => + actionItems.push( + + onClickUpdate({ + groupNumber, + query, + selectedGroup, + status: workflowStatus as AlertWorkflowStatus, + tableId, + }) + } + > + {statusArr[workflowStatus]} + + ) ); } } @@ -259,6 +288,4 @@ export const useGroupTakeActionsItems = ({ return getActionItems; }, [currentStatus, onClickUpdate, showAlertStatusActions]); - - return items; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts new file mode 100644 index 0000000000000..9e0b4e63715aa --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/mock.ts @@ -0,0 +1,1736 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockAlertSearchResponse } from '../../../../common/components/alerts_treemap/lib/mocks/mock_alert_search_response'; + +export const groupingSearchResponse = { + ruleName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 6048, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 32, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: ['critical hosts [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate]|f', + doc_count: 300, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 300, + }, + { + key: 'rule', + doc_count: 300, + }, + ], + }, + unitsCount: { + value: 300, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 300, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['high users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['low users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['medium users [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium users [Duplicate] [Duplicate]|f', + doc_count: 273, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 273, + }, + { + key: 'rule', + doc_count: 273, + }, + ], + }, + unitsCount: { + value: 273, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 273, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + { + key: ['critical hosts', 'f'], + key_as_string: 'critical hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts', 'f'], + key_as_string: 'high hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['high hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'high hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'high', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts ', 'f'], + key_as_string: 'low hosts |f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['low hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'low hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'low', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts', 'f'], + key_as_string: 'medium hosts|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['medium hosts [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'medium hosts [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 100, + hostsCountAggregation: { + value: 30, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 100, + }, + { + key: 'rule', + doc_count: 100, + }, + ], + }, + unitsCount: { + value: 100, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'medium', + doc_count: 100, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: ['critical users [Duplicate] [Duplicate] [Duplicate]', 'f'], + key_as_string: 'critical users [Duplicate] [Duplicate] [Duplicate]|f', + doc_count: 91, + hostsCountAggregation: { + value: 10, + }, + ruleTags: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'cool', + doc_count: 91, + }, + { + key: 'rule', + doc_count: 91, + }, + ], + }, + unitsCount: { + value: 91, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 91, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 91, + }, + }, + ], + }, + unitsCount: { + value: 6048, + }, + }, + }, + hostName: { + ...mockAlertSearchResponse, + hits: { + total: { + value: 900, + relation: 'eq', + }, + max_score: null, + hits: [], + }, + aggregations: { + groupsCount: { + value: 40, + }, + groupByFields: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'Host-f0m6ngo8fo', + doc_count: 75, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 75, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 75, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 25, + }, + }, + { + key: 'Host-4aijlqggv8', + doc_count: 63, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 63, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 63, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 21, + }, + }, + { + key: 'Host-e50lhbdm91', + doc_count: 51, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 51, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 51, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 17, + }, + }, + { + key: 'sqp', + doc_count: 42, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 42, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 42, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'sUl', + doc_count: 33, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 33, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 33, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'vLJ', + doc_count: 30, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 30, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 30, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-n28uwmsqmd', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 9, + }, + }, + { + key: 'JaE', + doc_count: 27, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 27, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 27, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'CUA', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'FWT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'ZqT', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'mmn', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xRS', + doc_count: 24, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 24, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 24, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'HiC', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Host-d7zbfvl3zz', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 7, + }, + }, + { + key: 'Nnc', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'OqH', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Vaw', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'XPg', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qBS', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'rwt', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'xVJ', + doc_count: 21, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 21, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 21, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'Bxg', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'efP', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + { + key: 'qcb', + doc_count: 18, + rulesCountAggregation: { + value: 3, + }, + unitsCount: { + value: 18, + }, + severitiesSubAggregation: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'critical', + doc_count: 18, + }, + ], + }, + countSeveritySubAggregation: { + value: 1, + }, + usersCountAggregation: { + value: 0, + }, + }, + ], + }, + unitsCount: { + value: 900, + }, + }, + }, +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts index 624b343c14cf9..921a8d3e3d43f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/grouping_settings/query_builder.ts @@ -42,8 +42,8 @@ export const getAlertsGroupingQuery = ({ getGroupingQuery({ additionalFilters, from, - groupByFields: !isNoneGroup(selectedGroup) ? getGroupFields(selectedGroup) : [], - statsAggregations: !isNoneGroup(selectedGroup) + groupByFields: !isNoneGroup([selectedGroup]) ? getGroupFields(selectedGroup) : [], + statsAggregations: !isNoneGroup([selectedGroup]) ? getAggregationsByGroupField(selectedGroup) : [], pageNumber: pageIndex * pageSize, @@ -51,7 +51,7 @@ export const getAlertsGroupingQuery = ({ { unitsCount: { value_count: { field: selectedGroup } }, }, - ...(!isNoneGroup(selectedGroup) + ...(!isNoneGroup([selectedGroup]) ? [{ groupsCount: { cardinality: { field: selectedGroup } } }] : []), ], diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx deleted file mode 100644 index 346e4b51df72d..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.test.tsx +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import type { Filter } from '@kbn/es-query'; -import useResizeObserver from 'use-resize-observer/polyfilled'; - -import '../../../common/mock/match_media'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, - TestProviders, -} from '../../../common/mock'; -import type { AlertsTableComponentProps } from './alerts_grouping'; -import { GroupedAlertsTableComponent } from './alerts_grouping'; -import { TableId } from '@kbn/securitysolution-data-table'; -import { useSourcererDataView } from '../../../common/containers/sourcerer'; -import type { UseFieldBrowserOptionsProps } from '../../../timelines/components/fields_browser'; -import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_context'; -import { mockTimelines } from '../../../common/mock/mock_timelines_plugin'; -import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; -import type { State } from '../../../common/store'; -import { createStore } from '../../../common/store'; -import { createStartServicesMock } from '../../../common/lib/kibana/kibana_react.mock'; -import { isNoneGroup, useGrouping } from '@kbn/securitysolution-grouping'; - -jest.mock('@kbn/securitysolution-grouping'); - -jest.mock('../../../common/containers/sourcerer'); -jest.mock('../../../common/containers/use_global_time', () => ({ - useGlobalTime: jest.fn().mockReturnValue({ - from: '2020-07-07T08:20:18.966Z', - isInitializing: false, - to: '2020-07-08T08:20:18.966Z', - setQuery: jest.fn(), - }), -})); - -jest.mock('./grouping_settings', () => ({ - getAlertsGroupingQuery: jest.fn(), - getDefaultGroupingOptions: () => [ - { label: 'ruleName', key: 'kibana.alert.rule.name' }, - { label: 'userName', key: 'user.name' }, - { label: 'hostName', key: 'host.name' }, - { label: 'sourceIP', key: 'source.ip' }, - ], - getSelectedGroupBadgeMetrics: jest.fn(), - getSelectedGroupButtonContent: jest.fn(), - getSelectedGroupCustomMetrics: jest.fn(), - useGroupTakeActionsItems: jest.fn(), -})); - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const original = jest.requireActual('react-redux'); - - return { - ...original, - useDispatch: () => mockDispatch, - }; -}); -jest.mock('../../../common/utils/normalize_time_range'); - -const mockUseFieldBrowserOptions = jest.fn(); -jest.mock('../../../timelines/components/fields_browser', () => ({ - useFieldBrowserOptions: (props: UseFieldBrowserOptionsProps) => mockUseFieldBrowserOptions(props), -})); - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -const mockFilterManager = createFilterManagerMock(); - -const mockKibanaServices = createStartServicesMock(); - -jest.mock('../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../common/lib/kibana'); - - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - ...mockKibanaServices, - application: { - navigateToUrl: jest.fn(), - capabilities: { - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - cases: { - ui: { getCasesContext: mockCasesContext }, - }, - uiSettings: { - get: jest.fn(), - }, - timelines: { ...mockTimelines }, - data: { - query: { - filterManager: mockFilterManager, - }, - }, - docLinks: { - links: { - siem: { - privileges: 'link', - }, - }, - }, - storage: { - get: jest.fn(), - set: jest.fn(), - }, - triggerActionsUi: { - getAlertsStateTable: jest.fn(() => <>), - alertsTableConfigurationRegistry: {}, - }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - remove: jest.fn(), - }), - }; -}); -const state: State = { - ...mockGlobalState, -}; -const { storage } = createSecuritySolutionStorageMock(); -const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - -const groupingStore = createStore( - { - ...state, - groups: { - groupSelector: <>, - selectedGroup: 'host.name', - }, - }, - SUB_PLUGINS_REDUCER, - kibanaObservable, - storage -); - -jest.mock('./timeline_actions/use_add_bulk_to_timeline', () => ({ - useAddBulkToTimelineAction: jest.fn(() => {}), -})); - -const sourcererDataView = { - indicesExist: true, - loading: false, - indexPattern: { - fields: [], - }, - browserFields: {}, -}; -const renderChildComponent = (groupingFilters: Filter[]) =>

; - -const testProps: AlertsTableComponentProps = { - defaultFilters: [], - from: '2020-07-07T08:20:18.966Z', - globalFilters: [], - globalQuery: { - query: 'query', - language: 'language', - }, - hasIndexMaintenance: true, - hasIndexWrite: true, - loading: false, - renderChildComponent, - runtimeMappings: {}, - signalIndexName: 'test', - tableId: TableId.test, - to: '2020-07-08T08:20:18.966Z', -}; - -const resetPagination = jest.fn(); - -describe('GroupedAlertsTable', () => { - const getGrouping = jest.fn().mockReturnValue(); - beforeEach(() => { - jest.clearAllMocks(); - (useSourcererDataView as jest.Mock).mockReturnValue({ - ...sourcererDataView, - selectedPatterns: ['myFakebeat-*'], - }); - (isNoneGroup as jest.Mock).mockReturnValue(true); - (useGrouping as jest.Mock).mockReturnValue({ - groupSelector: <>, - getGrouping, - selectedGroup: 'host.name', - pagination: { pageSize: 1, pageIndex: 0, reset: resetPagination }, - }); - }); - - it('calls the proper initial dispatch actions for groups', () => { - render( - - - - ); - expect(mockDispatch).toHaveBeenCalledTimes(2); - expect(mockDispatch.mock.calls[0][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_GROUP_SELECTOR' - ); - expect(mockDispatch.mock.calls[1][0].type).toEqual( - 'x-pack/security_solution/groups/UPDATE_SELECTED_GROUP' - ); - }); - - it('renders grouping table', async () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - - const { getByTestId } = render( - - - - ); - expect(getByTestId('grouping-table')).toBeInTheDocument(); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(false); - }); - - it('renders loading when expected', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - render( - - - - ); - expect(getGrouping.mock.calls[0][0].isLoading).toEqual(true); - }); - - it('resets grouping pagination when global query updates', () => { - (isNoneGroup as jest.Mock).mockReturnValue(false); - const { rerender } = render( - - - - ); - // called on initial query definition - expect(resetPagination).toHaveBeenCalledTimes(1); - rerender( - - - - ); - expect(resetPagination).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx index 02566a8302937..084be38391a45 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_field/index.tsx @@ -16,7 +16,6 @@ import type { ActionVariables, NotifyWhenSelectOptions, } from '@kbn/triggers-actions-ui-plugin/public'; -import { RuleNotifyWhen } from '@kbn/alerting-plugin/common'; import type { RuleAction, RuleActionAlertsFilterProperty, @@ -24,6 +23,7 @@ import type { } from '@kbn/alerting-plugin/common'; import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; +import { NOTIFICATION_DEFAULT_FREQUENCY } from '../../../../../common/constants'; import type { FieldHook } from '../../../../shared_imports'; import { useFormContext } from '../../../../shared_imports'; import { useKibana } from '../../../../common/lib/kibana'; @@ -33,12 +33,6 @@ import { FORM_ON_ACTIVE_ALERT_OPTION, } from './translations'; -const DEFAULT_FREQUENCY = { - notifyWhen: RuleNotifyWhen.ACTIVE, - throttle: null, - summary: true, -}; - const NOTIFY_WHEN_OPTIONS: NotifyWhenSelectOptions[] = [ { isSummaryOption: true, @@ -193,12 +187,35 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = const setActionAlertsFilterProperty = useCallback( (key: string, value: RuleActionAlertsFilterProperty, index: number) => { + field.setValue((prevValue: RuleAction[]) => { + const updatedActions = [...prevValue]; + const { alertsFilter, ...rest } = updatedActions[index]; + const updatedAlertsFilter = { ...alertsFilter }; + + if (value) { + updatedAlertsFilter[key] = value; + } else { + delete updatedAlertsFilter[key]; + } + + updatedActions[index] = { + ...rest, + ...(!isEmpty(updatedAlertsFilter) ? { alertsFilter: updatedAlertsFilter } : {}), + }; + return updatedActions; + }); + }, + [field] + ); + + const setActionFrequency = useCallback( + (key: string, value: RuleActionParam, index: number) => { field.setValue((prevValue: RuleAction[]) => { const updatedActions = [...prevValue]; updatedActions[index] = { ...updatedActions[index], - alertsFilter: { - ...(updatedActions[index].alertsFilter ?? { query: null, timeframe: null }), + frequency: { + ...(updatedActions[index].frequency ?? NOTIFICATION_DEFAULT_FREQUENCY), [key]: value, }, }; @@ -217,22 +234,22 @@ export const RuleActionsField: React.FC = ({ field, messageVariables }) = setActionIdByIndex, setActions: setAlertActionsProperty, setActionParamsProperty, - setActionFrequencyProperty: () => {}, + setActionFrequencyProperty: setActionFrequency, setActionAlertsFilterProperty, featureId: SecurityConnectorFeatureId, defaultActionMessage: DEFAULT_ACTION_MESSAGE, defaultSummaryMessage: DEFAULT_ACTION_MESSAGE, hideActionHeader: true, - hideNotifyWhen: true, hasSummary: true, notifyWhenSelectOptions: NOTIFY_WHEN_OPTIONS, - defaultRuleFrequency: DEFAULT_FREQUENCY, + defaultRuleFrequency: NOTIFICATION_DEFAULT_FREQUENCY, showActionAlertsFilter: true, }), [ actions, getActionForm, messageVariables, + setActionFrequency, setActionIdByIndex, setActionParamsProperty, setAlertActionsProperty, diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx index f5345f42f810b..aca8fe27abd0f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/index.tsx @@ -96,7 +96,13 @@ const RuleActionsOverflowComponent = ({ ids: [rule.id], duplicatePayload: { include_exceptions: - modalDuplicationConfirmationResult === DuplicateOptions.withExceptions, + modalDuplicationConfirmationResult === DuplicateOptions.withExceptions || + modalDuplicationConfirmationResult === + DuplicateOptions.withExceptionsExcludeExpiredExceptions, + include_expired_exceptions: !( + modalDuplicationConfirmationResult === + DuplicateOptions.withExceptionsExcludeExpiredExceptions + ), }, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts index 858578f8a5d38..f16cac0eb923a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/get_schema.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { i18n } from '@kbn/i18n'; - import type { ActionTypeRegistryContract } from '@kbn/triggers-actions-ui-plugin/public'; import { debouncedValidateRuleActionsField } from '../../../containers/detection_engine/rules/validate_rule_actions_field'; @@ -30,12 +28,4 @@ export const getSchema = ({ responseActions: {}, enabled: {}, kibanaSiemAppUrl: {}, - throttle: { - label: i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', - { - defaultMessage: 'Actions frequency', - } - ), - }, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx index 58dd95bcac0dd..86bbb7604add2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/index.tsx @@ -15,7 +15,6 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; -import { findIndex } from 'lodash/fp'; import type { FC } from 'react'; import React, { memo, useCallback, useEffect, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -29,20 +28,13 @@ import { ResponseActionsForm } from '../../../../detection_engine/rule_response_ import type { RuleStepProps, ActionsStepRule } from '../../../pages/detection_engine/rules/types'; import { RuleStep } from '../../../pages/detection_engine/rules/types'; import { StepRuleDescription } from '../description_step'; -import { Form, UseField, useForm, useFormData } from '../../../../shared_imports'; +import { Form, UseField, useForm } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; -import { - ThrottleSelectField, - THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, - DEFAULT_THROTTLE_OPTION, -} from '../throttle_select_field'; import { RuleActionsField } from '../rule_actions_field'; import { useKibana } from '../../../../common/lib/kibana'; import { getSchema } from './get_schema'; import * as I18n from './translations'; import { APP_UI_ID } from '../../../../../common/constants'; -import { useManageCaseAction } from './use_manage_case_action'; -import { THROTTLE_FIELD_HELP_TEXT, THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY } from './translations'; interface StepRuleActionsProps extends RuleStepProps { defaultValues?: ActionsStepRule | null; @@ -55,23 +47,10 @@ export const stepActionsDefaultValue: ActionsStepRule = { actions: [], responseActions: [], kibanaSiemAppUrl: '', - throttle: DEFAULT_THROTTLE_OPTION.value, }; const GhostFormField = () => <>; -const getThrottleOptions = (throttle?: string | null) => { - // Add support for throttle options set by the API - if ( - throttle && - findIndex(['value', throttle], THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING) < 0 - ) { - return [...THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING, { value: throttle, text: throttle }]; - } - - return THROTTLE_OPTIONS_FOR_RULE_CREATION_AND_EDITING; -}; - const DisplayActionsHeader = () => { return ( <> @@ -99,7 +78,6 @@ const StepRuleActionsComponent: FC = ({ actionMessageParams, ruleType, }) => { - const [isLoadingCaseAction] = useManageCaseAction(); const { services: { application, @@ -127,11 +105,6 @@ const StepRuleActionsComponent: FC = ({ schema, }); const { getFields, getFormData, submit } = form; - const [{ throttle: formThrottle }] = useFormData({ - form, - watch: ['throttle'], - }); - const throttle = formThrottle || initialState.throttle; const handleSubmit = useCallback( (enabled: boolean) => { @@ -163,44 +136,20 @@ const StepRuleActionsComponent: FC = ({ }; }, [getData, setForm]); - const throttleOptions = useMemo(() => { - return getThrottleOptions(throttle); - }, [throttle]); - - const throttleFieldComponentProps = useMemo( - () => ({ - idAria: 'detectionEngineStepRuleActionsThrottle', - isDisabled: isLoading, - isLoading: isLoadingCaseAction, - dataTestSubj: 'detectionEngineStepRuleActionsThrottle', - hasNoInitialSelection: false, - helpText: isQueryRule(ruleType) - ? THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY - : THROTTLE_FIELD_HELP_TEXT, - euiFieldProps: { - options: throttleOptions, - }, - }), - [isLoading, isLoadingCaseAction, ruleType, throttleOptions] - ); - const displayActionsOptions = useMemo( - () => - throttle !== stepActionsDefaultValue.throttle ? ( - <> - - - - ) : ( - - ), - [throttle, actionMessageParams] + () => ( + <> + + + + ), + [actionMessageParams] ); const displayResponseActionsOptions = useMemo(() => { if (isQueryRule(ruleType)) { @@ -217,11 +166,6 @@ const StepRuleActionsComponent: FC = ({ return application.capabilities.actions.show ? ( <> - {displayActionsOptions} {responseActionsEnabled && displayResponseActionsOptions} @@ -231,14 +175,6 @@ const StepRuleActionsComponent: FC = ({ ) : ( <> {I18n.NO_ACTIONS_READ_PERMISSIONS} - - - - ); }, [ @@ -246,7 +182,6 @@ const StepRuleActionsComponent: FC = ({ displayActionsOptions, displayResponseActionsOptions, responseActionsEnabled, - throttleFieldComponentProps, ]); if (isReadOnlyView) { diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx index 15937883fb409..d467c3af05f8f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/translations.tsx @@ -28,19 +28,3 @@ export const NO_ACTIONS_READ_PERMISSIONS = i18n.translate( 'Cannot create rule actions. You do not have "Read" permissions for the "Actions" plugin.', } ); - -export const THROTTLE_FIELD_HELP_TEXT = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true.', - } -); - -export const THROTTLE_FIELD_HELP_TEXT_WHEN_QUERY = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpTextWhenQuery', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true. This frequency does not apply to Response Actions.', - } -); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx deleted file mode 100644 index 57dd5670dba80..0000000000000 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_rule_actions/use_manage_case_action.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useRef, useState } from 'react'; -import { getAllConnectorsUrl, getCreateConnectorUrl } from '@kbn/cases-plugin/common'; -import { convertArrayToCamelCase, KibanaServices } from '../../../../common/lib/kibana'; - -interface CaseAction { - connectorTypeId: string; - id: string; - isPreconfigured: boolean; - name: string; - referencedByCount: number; -} - -const CASE_ACTION_NAME = 'Cases'; - -export const useManageCaseAction = () => { - const hasInit = useRef(true); - const [loading, setLoading] = useState(true); - const [hasError, setHasError] = useState(false); - - useEffect(() => { - const abortCtrl = new AbortController(); - const fetchActions = async () => { - try { - const actions = convertArrayToCamelCase( - await KibanaServices.get().http.fetch(getAllConnectorsUrl(), { - method: 'GET', - signal: abortCtrl.signal, - }) - ) as CaseAction[]; - - if (!actions.some((a) => a.connectorTypeId === '.case' && a.name === CASE_ACTION_NAME)) { - await KibanaServices.get().http.post(getCreateConnectorUrl(), { - method: 'POST', - body: JSON.stringify({ - connector_type_id: '.case', - config: {}, - name: CASE_ACTION_NAME, - secrets: {}, - }), - signal: abortCtrl.signal, - }); - } - setLoading(false); - } catch { - setLoading(false); - setHasError(true); - } - }; - if (hasInit.current) { - hasInit.current = false; - fetchActions(); - } - - return () => { - abortCtrl.abort(); - }; - }, []); - return [loading, hasError]; -}; diff --git a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx index 78d736e99c93e..92f77c3e2df91 100644 --- a/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx +++ b/x-pack/plugins/security_solution/public/detections/hooks/trigger_actions_alert_table/use_persistent_controls.tsx @@ -7,7 +7,6 @@ import React, { useCallback, useMemo } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { isNoneGroup } from '@kbn/securitysolution-grouping'; import { dataTableSelectors, tableDefaults, @@ -29,9 +28,6 @@ export const getPersistentControlsHook = (tableId: TableId) => { const getGroupSelector = groupSelectors.getGroupSelector(); const groupSelector = useSelector((state: State) => getGroupSelector(state)); - const getSelectedGroup = groupSelectors.getSelectedGroup(); - - const selectedGroup = useSelector((state: State) => getSelectedGroup(state)); const getTable = useMemo(() => dataTableSelectors.getTableByIdSelector(), []); @@ -88,10 +84,10 @@ export const getPersistentControlsHook = (tableId: TableId) => { hasRightOffset={false} additionalFilters={additionalFiltersComponent} showInspect={false} - additionalMenuOptions={isNoneGroup(selectedGroup) ? [groupSelector] : []} + additionalMenuOptions={groupSelector != null ? [groupSelector] : []} /> ), - [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector, selectedGroup] + [tableView, handleChangeTableView, additionalFiltersComponent, groupSelector] ); return { diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index 64e489599dd05..13836e658eee7 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -5,10 +5,9 @@ * 2.0. */ -import React from 'react'; -import { mount } from 'enzyme'; +import React, { useEffect } from 'react'; +import { render, waitFor } from '@testing-library/react'; import { useParams } from 'react-router-dom'; -import { waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { createSecuritySolutionStorageMock, @@ -29,6 +28,10 @@ import { mockCasesContext } from '@kbn/cases-plugin/public/mocks/mock_cases_cont import { createFilterManagerMock } from '@kbn/data-plugin/public/query/filter_manager/filter_manager.mock'; import { dataViewPluginMocks } from '@kbn/data-views-plugin/public/mocks'; import { createStubDataView } from '@kbn/data-views-plugin/common/data_view.stub'; +import { useListsConfig } from '../../containers/detection_engine/lists/use_lists_config'; +import type { FilterGroupProps } from '../../../common/components/filter_group/types'; +import { FilterGroup } from '../../../common/components/filter_group'; +import type { AlertsTableComponentProps } from '../../components/alerts_table/alerts_grouping'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar @@ -38,6 +41,27 @@ jest.mock('../../../common/components/search_bar', () => ({ jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); +jest.mock('../../../common/hooks/use_space_id', () => ({ + useSpaceId: () => 'default', +})); +jest.mock('../../../common/components/filter_group'); + +const mockStatusCapture = jest.fn(); +const GroupedAlertsTable: React.FC = ({ + currentAlertStatusFilterValue, +}) => { + useEffect(() => { + if (currentAlertStatusFilterValue) { + mockStatusCapture(currentAlertStatusFilterValue); + } + }, [currentAlertStatusFilterValue]); + return ; +}; + +jest.mock('../../components/alerts_table/alerts_grouping', () => ({ + GroupedAlertsTable, +})); + jest.mock('../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../components/user_info'); jest.mock('../../../common/containers/sourcerer'); @@ -158,9 +182,11 @@ jest.mock('../../../common/components/page/use_refetch_by_session'); describe('DetectionEnginePageComponent', () => { beforeAll(() => { + (useListsConfig as jest.Mock).mockReturnValue({ loading: false, needsConfiguration: false }); (useParams as jest.Mock).mockReturnValue({}); (useUserData as jest.Mock).mockReturnValue([ { + loading: false, hasIndexRead: true, canUserREAD: true, }, @@ -170,10 +196,15 @@ describe('DetectionEnginePageComponent', () => { indexPattern: {}, browserFields: mockBrowserFields, }); + (FilterGroup as jest.Mock).mockImplementation(() => { + return ; + }); + }); + beforeEach(() => { + jest.clearAllMocks(); }); - it('renders correctly', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -181,12 +212,12 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('FiltersGlobal').exists()).toBe(true); + expect(getByTestId('filter-group__loading')).toBeInTheDocument(); }); }); it('renders the chart panels', async () => { - const wrapper = mount( + const { getByTestId } = render( @@ -195,7 +226,119 @@ describe('DetectionEnginePageComponent', () => { ); await waitFor(() => { - expect(wrapper.find('[data-test-subj="chartPanels"]').exists()).toBe(true); + expect(getByTestId('chartPanels')).toBeInTheDocument(); + }); + }); + + it('the pageFiltersUpdateHandler updates status when a multi status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + params: ['open', 'acknowledged'], + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open', 'acknowledged']); + }); + + it('the pageFiltersUpdateHandler updates status when a single status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.workflow_status', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.workflow_status': 'open', + }, + }, + }, + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); + }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, ['open']); + }); + + it('the pageFiltersUpdateHandler clears status when no status filter is passed', async () => { + (FilterGroup as jest.Mock).mockImplementationOnce(({ onFilterChange }: FilterGroupProps) => { + if (onFilterChange) { + // once with status + onFilterChange([ + { + meta: { + index: 'security-solution-default', + key: 'kibana.alert.severity', + disabled: false, + }, + query: { + match_phrase: { + 'kibana.alert.severity': 'low', + }, + }, + }, + ]); + } + return ; + }); + await waitFor(() => { + render( + + + + + + ); }); + // when statusFilter updates, we call mockStatusCapture in test mocks + expect(mockStatusCapture).toHaveBeenNthCalledWith(1, []); + expect(mockStatusCapture).toHaveBeenNthCalledWith(2, []); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index 0eacecd46e8fc..1ccfaea4584ee 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -29,7 +29,6 @@ import { dataTableActions, dataTableSelectors, tableDefaults, - FILTER_OPEN, TableId, } from '@kbn/securitysolution-data-table'; import { ALERTS_TABLE_REGISTRY_CONFIG_IDS } from '../../../../common/constants'; @@ -139,7 +138,7 @@ const DetectionEnginePageComponent: React.FC = ({ const arePageFiltersEnabled = useIsExperimentalFeatureEnabled('alertsPageFiltersEnabled'); // when arePageFiltersEnabled === false - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const [statusFilter, setStatusFilter] = useState([]); const updatedAt = useShallowEqualSelector( (state) => (getTable(state, TableId.alertsOnAlertsPage) ?? tableDefaults).updated @@ -177,8 +176,8 @@ const DetectionEnginePageComponent: React.FC = ({ if (arePageFiltersEnabled) { return detectionPageFilters; } - return buildAlertStatusFilter(filterGroup); - }, [filterGroup, detectionPageFilters, arePageFiltersEnabled]); + return buildAlertStatusFilter(statusFilter[0] ?? 'open'); + }, [statusFilter, detectionPageFilters, arePageFiltersEnabled]); useEffect(() => { if (!detectionPageFilterHandler) return; @@ -276,6 +275,19 @@ const DetectionEnginePageComponent: React.FC = ({ const pageFiltersUpdateHandler = useCallback((newFilters: Filter[]) => { setDetectionPageFilters(newFilters); + if (newFilters.length) { + const newStatusFilter = newFilters.find( + (filter) => filter.meta.key === 'kibana.alert.workflow_status' + ); + if (newStatusFilter) { + const status: Status[] = newStatusFilter.meta.params + ? (newStatusFilter.meta.params as Status[]) + : [newStatusFilter.query?.match_phrase['kibana.alert.workflow_status']]; + setStatusFilter(status); + } else { + setStatusFilter([]); + } + } }, []); // Callback for when open/closed filter changes @@ -284,9 +296,9 @@ const DetectionEnginePageComponent: React.FC = ({ const timelineId = TableId.alertsOnAlertsPage; clearEventsLoading({ id: timelineId }); clearEventsDeleted({ id: timelineId }); - setFilterGroup(newFilterGroup); + setStatusFilter([newFilterGroup]); }, - [clearEventsLoading, clearEventsDeleted, setFilterGroup] + [clearEventsLoading, clearEventsDeleted, setStatusFilter] ); const areDetectionPageFiltersLoading = useMemo(() => { @@ -317,7 +329,7 @@ const DetectionEnginePageComponent: React.FC = ({ @@ -352,7 +364,7 @@ const DetectionEnginePageComponent: React.FC = ({ [ arePageFiltersEnabled, dataViewId, - filterGroup, + statusFilter, filters, onFilterGroupChangedCallback, pageFiltersUpdateHandler, @@ -462,7 +474,7 @@ const DetectionEnginePageComponent: React.FC = ({ { moment.suppressDeprecationWarnings = true; @@ -146,7 +147,6 @@ describe('rule helpers', () => { const scheduleRuleStepData = { from: '0s', interval: '5m' }; const ruleActionsStepData = { enabled: true, - throttle: 'no_actions', actions: [], responseActions: undefined, }; @@ -410,16 +410,22 @@ describe('rule helpers', () => { describe('getActionsStepsData', () => { test('returns expected ActionsStepRule rule object', () => { + const actions: RuleAlertAction[] = [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + ]; const mockedRule = { ...mockRule('test-id'), - actions: [ - { - id: 'id', - group: 'group', - params: {}, - action_type_id: 'action_type_id', - }, - ], + actions, }; const result: ActionsStepRule = getActionsStepsData(mockedRule); const expected = { @@ -429,11 +435,15 @@ describe('rule helpers', () => { group: 'group', params: {}, actionTypeId: 'action_type_id', + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, }, ], responseActions: undefined, enabled: mockedRule.enabled, - throttle: 'no_actions', }; expect(result).toEqual(expected); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx index a290ac92f6a66..d0cfdc8b707f3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/helpers.tsx @@ -76,12 +76,11 @@ export const getActionsStepsData = ( response_actions?: ResponseAction[]; } ): ActionsStepRule => { - const { enabled, throttle, meta, actions = [], response_actions: responseActions } = rule; + const { enabled, meta, actions = [], response_actions: responseActions } = rule; return { actions: actions?.map(transformRuleToAlertAction), responseActions: responseActions?.map(transformRuleToAlertResponseAction), - throttle, kibanaSiemAppUrl: meta?.kibana_siem_app_url, enabled, }; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts index edbb3b4ecbf97..2d2c7580ed051 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/types.ts @@ -194,7 +194,6 @@ export interface ActionsStepRule { responseActions?: RuleResponseAction[]; enabled: boolean; kibanaSiemAppUrl?: string; - throttle?: string | null; } export interface DefineStepRuleJson { diff --git a/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.test.tsx b/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.test.tsx new file mode 100644 index 0000000000000..72ba17ad86620 --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.test.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; + +import { ExceptionsListCard } from '.'; +import { useListDetailsView } from '../../hooks'; +import { useExceptionsListCard } from '../../hooks/use_exceptions_list.card'; +import { getExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; +import { getExceptionListItemSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_item_schema.mock'; +import { TestProviders } from '../../../common/mock'; + +jest.mock('../../hooks'); +jest.mock('../../hooks/use_exceptions_list.card'); + +const getMockUseExceptionsListCard = () => ({ + listId: 'my-list', + listName: 'Exception list', + listType: 'detection', + createdAt: '2023-02-01T10:20:30.000Z', + createdBy: 'elastic', + exceptions: [{ ...getExceptionListItemSchemaMock() }], + pagination: { pageIndex: 0, pageSize: 5, totalItemCount: 1 }, + ruleReferences: { + 'my-list': { + name: 'Exception list', + id: '345', + referenced_rules: [], + listId: 'my-list', + }, + }, + toggleAccordion: false, + openAccordionId: '123', + menuActionItems: [ + { + key: 'Export', + icon: 'exportAction', + label: 'Export', + onClick: jest.fn(), + }, + ], + listRulesCount: '5', + listDescription: 'My exception list description', + exceptionItemsCount: jest.fn(), + onEditExceptionItem: jest.fn(), + onDeleteException: jest.fn(), + onPaginationChange: jest.fn(), + setToggleAccordion: jest.fn(), + exceptionViewerStatus: '', + showAddExceptionFlyout: false, + showEditExceptionFlyout: false, + exceptionToEdit: undefined, + onAddExceptionClick: jest.fn(), + handleConfirmExceptionFlyout: jest.fn(), + handleCancelExceptionItemFlyout: jest.fn(), + goToExceptionDetail: jest.fn(), + emptyViewerTitle: 'Empty View', + emptyViewerBody: 'This is the empty view description.', + emptyViewerButtonText: 'Take action', + handleCancelExpiredExceptionsModal: jest.fn(), + handleConfirmExpiredExceptionsModal: jest.fn(), + showIncludeExpiredExceptionsModal: false, +}); +const getMockUseListDetailsView = () => ({ + linkedRules: [], + showManageRulesFlyout: false, + showManageButtonLoader: false, + disableManageButton: false, + onManageRules: jest.fn(), + onSaveManageRules: jest.fn(), + onCancelManageRules: jest.fn(), + onRuleSelectionChange: jest.fn(), +}); + +describe('ExceptionsListCard', () => { + beforeEach(() => { + (useExceptionsListCard as jest.Mock).mockReturnValue(getMockUseExceptionsListCard()); + (useListDetailsView as jest.Mock).mockReturnValue(getMockUseListDetailsView()); + }); + afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + }); + + it('should display expired exception confirmation modal when "showIncludeExpiredExceptionsModal" is "true"', () => { + (useExceptionsListCard as jest.Mock).mockReturnValue({ + ...getMockUseExceptionsListCard(), + showIncludeExpiredExceptionsModal: true, + }); + + const wrapper = render( + + + + ); + expect(wrapper.getByTestId('includeExpiredExceptionsConfirmationModal')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx index 95dde7239b605..44370b274cd12 100644 --- a/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/components/exceptions_list_card/index.tsx @@ -33,7 +33,7 @@ import { ListExceptionItems } from '../list_exception_items'; import { useListDetailsView } from '../../hooks'; import { useExceptionsListCard } from '../../hooks/use_exceptions_list.card'; import { ManageRules } from '../manage_rules'; -import { ExportExceptionsListModal } from '../export_exceptions_list_modal'; +import { IncludeExpiredExceptionsModal } from '../expired_exceptions_list_items_modal'; interface ExceptionsListCardProps { exceptionsList: ExceptionListInfo; @@ -59,6 +59,17 @@ interface ExceptionsListCardProps { name: string; namespaceType: NamespaceType; }) => () => Promise; + handleDuplicate: ({ + includeExpiredExceptions, + listId, + name, + namespaceType, + }: { + includeExpiredExceptions: boolean; + listId: string; + name: string; + namespaceType: NamespaceType; + }) => () => Promise; readOnly: boolean; } const buttonCss = css` @@ -78,7 +89,7 @@ const ListHeaderContainer = styled(EuiFlexGroup)` text-align: initial; `; export const ExceptionsListCard = memo( - ({ exceptionsList, handleDelete, handleExport, readOnly }) => { + ({ exceptionsList, handleDelete, handleExport, handleDuplicate, readOnly }) => { const { linkedRules, showManageRulesFlyout, @@ -119,13 +130,14 @@ export const ExceptionsListCard = memo( emptyViewerTitle, emptyViewerBody, emptyViewerButtonText, - handleCancelExportModal, - handleConfirmExportModal, - showExportModal, + handleCancelExpiredExceptionsModal, + handleConfirmExpiredExceptionsModal, + showIncludeExpiredExceptionsModal, } = useExceptionsListCard({ exceptionsList, handleExport, handleDelete, + handleDuplicate, handleManageRules: onManageRules, }); @@ -259,10 +271,11 @@ export const ExceptionsListCard = memo( onRuleSelectionChange={onRuleSelectionChange} /> ) : null} - {showExportModal ? ( - ) : null} diff --git a/x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.test.tsx b/x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.test.tsx new file mode 100644 index 0000000000000..43ebd8197c8d5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { IncludeExpiredExceptionsModal } from '.'; +import { fireEvent, render } from '@testing-library/react'; + +describe('IncludeExpiredExceptionsModal', () => { + const handleCloseModal = jest.fn(); + const onModalConfirm = jest.fn(); + + it('should call handleCloseModal on cancel click', () => { + const wrapper = render( + + ); + fireEvent.click(wrapper.getByTestId('confirmModalCancelButton')); + expect(handleCloseModal).toHaveBeenCalled(); + }); + + it('should call onModalConfirm on confirm click', () => { + const wrapper = render( + + ); + fireEvent.click(wrapper.getByTestId('confirmModalConfirmButton')); + expect(onModalConfirm).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.tsx new file mode 100644 index 0000000000000..17588e6b305a7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/exceptions/components/expired_exceptions_list_items_modal/index.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback, useState } from 'react'; + +import { EuiConfirmModal, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; +import * as i18n from '../../translations'; + +export const CHECK_EXCEPTION_TTL_ACTION_TYPES = { + DUPLICATE: 'duplicate', + EXPORT: 'export', +} as const; + +export type CheckExceptionTtlActionTypes = + typeof CHECK_EXCEPTION_TTL_ACTION_TYPES[keyof typeof CHECK_EXCEPTION_TTL_ACTION_TYPES]; + +interface IncludeExpiredExceptionsModalProps { + handleCloseModal: () => void; + onModalConfirm: (includeExpired: boolean) => void; + action: CheckExceptionTtlActionTypes; +} + +export const IncludeExpiredExceptionsModal = memo( + ({ handleCloseModal, onModalConfirm, action }) => { + const [includeExpired, setIncludeExpired] = useState(true); + + const handleSwitchChange = useCallback(() => { + setIncludeExpired(!includeExpired); + }, [setIncludeExpired, includeExpired]); + + const handleConfirm = useCallback(() => { + onModalConfirm(includeExpired); + handleCloseModal(); + }, [includeExpired, handleCloseModal, onModalConfirm]); + + return ( + + + {action === CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT + ? i18n.EXPIRED_EXCEPTIONS_MODAL_EXPORT_DESCRIPTION + : i18n.EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_DESCRIPTION} + + + + + ); + } +); + +IncludeExpiredExceptionsModal.displayName = 'IncludeExpiredExceptionsModal'; diff --git a/x-pack/plugins/security_solution/public/exceptions/components/export_exceptions_list_modal/index.tsx b/x-pack/plugins/security_solution/public/exceptions/components/export_exceptions_list_modal/index.tsx deleted file mode 100644 index 7316925cc24cf..0000000000000 --- a/x-pack/plugins/security_solution/public/exceptions/components/export_exceptions_list_modal/index.tsx +++ /dev/null @@ -1,50 +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, { memo, useCallback, useState } from 'react'; - -import { EuiConfirmModal, EuiSwitch } from '@elastic/eui'; -import * as i18n from '../../translations'; - -interface ExportExceptionsListModalProps { - handleCloseModal: () => void; - onModalConfirm: (includeExpired: boolean) => void; -} - -export const ExportExceptionsListModal = memo( - ({ handleCloseModal, onModalConfirm }) => { - const [exportExpired, setExportExpired] = useState(true); - - const handleSwitchChange = useCallback(() => { - setExportExpired(!exportExpired); - }, [setExportExpired, exportExpired]); - - const handleConfirm = useCallback(() => { - onModalConfirm(exportExpired); - handleCloseModal(); - }, [exportExpired, handleCloseModal, onModalConfirm]); - - return ( - - - - ); - } -); - -ExportExceptionsListModal.displayName = 'ExportExceptionsListModal'; diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx b/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx index 27ce5f3604c2f..82440e478edcd 100644 --- a/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_exceptions_list.card/index.tsx @@ -20,7 +20,15 @@ import type { ExceptionListInfo } from '../use_all_exception_lists'; import { useListExceptionItems } from '../use_list_exception_items'; import * as i18n from '../../translations'; import { checkIfListCannotBeEdited } from '../../utils/list.utils'; +import type { CheckExceptionTtlActionTypes } from '../../components/expired_exceptions_list_items_modal'; +import { CHECK_EXCEPTION_TTL_ACTION_TYPES } from '../../components/expired_exceptions_list_items_modal'; +interface DuplicateListAction { + listId: string; + name: string; + namespaceType: NamespaceType; + includeExpiredExceptions: boolean; +} interface ExportListAction { id: string; listId: string; @@ -37,6 +45,7 @@ export const useExceptionsListCard = ({ exceptionsList, handleExport, handleDelete, + handleDuplicate, handleManageRules, }: { exceptionsList: ExceptionListInfo; @@ -48,13 +57,20 @@ export const useExceptionsListCard = ({ includeExpiredExceptions, }: ExportListAction) => () => Promise; handleDelete: ({ id, listId, namespaceType }: ListAction) => () => Promise; + handleDuplicate: ({ + listId, + name, + namespaceType, + includeExpiredExceptions, + }: DuplicateListAction) => () => Promise; handleManageRules: () => void; }) => { const [viewerStatus, setViewerStatus] = useState(ViewerStatus.LOADING); const [exceptionToEdit, setExceptionToEdit] = useState(); const [showAddExceptionFlyout, setShowAddExceptionFlyout] = useState(false); const [showEditExceptionFlyout, setShowEditExceptionFlyout] = useState(false); - const [showExportModal, setShowExportModal] = useState(false); + const [showIncludeExpiredExceptionsModal, setShowIncludeExpiredExceptionsModal] = + useState(null); const { name: listName, @@ -135,10 +151,19 @@ export const useExceptionsListCard = ({ includeExpiredExceptions: true, })(); } else { - setShowExportModal(true); + setShowIncludeExpiredExceptionsModal(CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT); } }, }, + { + key: 'Duplicate', + icon: 'copy', + label: i18n.DUPLICATE_EXCEPTION_LIST, + disabled: listCannotBeEdited, + onClick: (_: React.MouseEvent) => { + setShowIncludeExpiredExceptionsModal(CHECK_EXCEPTION_TTL_ACTION_TYPES.DUPLICATE); + }, + }, { key: 'Delete', icon: 'trash', @@ -163,16 +188,15 @@ export const useExceptionsListCard = ({ }, ], [ + listCannotBeEdited, + listType, + handleExport, exceptionsList.id, exceptionsList.list_id, exceptionsList.name, exceptionsList.namespace_type, handleDelete, - setShowExportModal, - listCannotBeEdited, handleManageRules, - handleExport, - listType, ] ); @@ -197,24 +221,42 @@ export const useExceptionsListCard = ({ ); const onExportListClick = useCallback(() => { - setShowExportModal(true); - }, [setShowExportModal]); + setShowIncludeExpiredExceptionsModal(CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT); + }, [setShowIncludeExpiredExceptionsModal]); - const handleCancelExportModal = () => { - setShowExportModal(false); + const handleCancelExpiredExceptionsModal = () => { + setShowIncludeExpiredExceptionsModal(null); }; - const handleConfirmExportModal = useCallback( + const handleConfirmExpiredExceptionsModal = useCallback( (includeExpiredExceptions: boolean): void => { - handleExport({ - id: exceptionsList.id, - listId: exceptionsList.list_id, - name: exceptionsList.name, - namespaceType: exceptionsList.namespace_type, - includeExpiredExceptions, - })(); + if (showIncludeExpiredExceptionsModal === CHECK_EXCEPTION_TTL_ACTION_TYPES.EXPORT) { + handleExport({ + id: exceptionsList.id, + listId: exceptionsList.list_id, + name: exceptionsList.name, + namespaceType: exceptionsList.namespace_type, + includeExpiredExceptions, + })(); + } + if (showIncludeExpiredExceptionsModal === CHECK_EXCEPTION_TTL_ACTION_TYPES.DUPLICATE) { + handleDuplicate({ + listId: exceptionsList.list_id, + name: exceptionsList.name, + namespaceType: exceptionsList.namespace_type, + includeExpiredExceptions, + })(); + } }, - [handleExport, exceptionsList] + [ + showIncludeExpiredExceptionsModal, + handleExport, + exceptionsList.id, + exceptionsList.list_id, + exceptionsList.name, + exceptionsList.namespace_type, + handleDuplicate, + ] ); // routes to x-pack/plugins/security_solution/public/exceptions/routes.tsx @@ -255,9 +297,9 @@ export const useExceptionsListCard = ({ emptyViewerTitle, emptyViewerBody, emptyViewerButtonText, - showExportModal, + showIncludeExpiredExceptionsModal, onExportListClick, - handleCancelExportModal, - handleConfirmExportModal, + handleCancelExpiredExceptionsModal, + handleConfirmExpiredExceptionsModal, }; }; diff --git a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts index 4c70b0dc4521b..99f3288379537 100644 --- a/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts +++ b/x-pack/plugins/security_solution/public/exceptions/hooks/use_list_detail_view/index.ts @@ -51,7 +51,7 @@ export const useListDetailsView = (exceptionListId: string) => { const { http, notifications } = services; const { navigateToApp } = services.application; - const { exportExceptionList, deleteExceptionList } = useApi(http); + const { exportExceptionList, deleteExceptionList, duplicateExceptionList } = useApi(http); const [{ loading: userInfoLoading, canUserCRUD, canUserREAD }] = useUserData(); @@ -190,6 +190,35 @@ export const useListDetailsView = (exceptionListId: string) => { [list, exportExceptionList, handleErrorStatus, toasts] ); + const onDuplicateList = useCallback( + async (includeExpiredExceptions: boolean) => { + try { + if (!list) return; + await duplicateExceptionList({ + listId: list.list_id, + includeExpiredExceptions, + namespaceType: list.namespace_type, + onError: (error: Error) => handleErrorStatus(error), + onSuccess: (newList: ExceptionListSchema) => { + toasts?.addSuccess(i18n.EXCEPTION_LIST_DUPLICATED_SUCCESSFULLY(list.name)); + navigateToApp(APP_UI_ID, { + deepLinkId: SecurityPageName.exceptions, + path: `/details/${newList.list_id}`, + }); + }, + }); + } catch (error) { + handleErrorStatus( + error, + undefined, + i18n.EXCEPTION_DUPLICATE_ERROR, + i18n.EXCEPTION_DUPLICATE_ERROR_DESCRIPTION + ); + } + }, + [list, duplicateExceptionList, handleErrorStatus, toasts, navigateToApp] + ); + const handleOnDownload = useCallback(() => { setExportedList(undefined); }, []); @@ -384,6 +413,7 @@ export const useListDetailsView = (exceptionListId: string) => { refreshExceptions, disableManageButton, handleDelete, + onDuplicateList, onEditListDetails, onExportList, onDeleteList, diff --git a/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx b/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx index 1e5b0af5768a4..023ebe69ab2fe 100644 --- a/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/pages/list_detail_view/index.tsx @@ -25,7 +25,8 @@ import { AutoDownload } from '../../../common/components/auto_download/auto_down import { ListWithSearch, ManageRules, ListDetailsLinkAnchor } from '../../components'; import { useListDetailsView } from '../../hooks'; import * as i18n from '../../translations'; -import { ExportExceptionsListModal } from '../../components/export_exceptions_list_modal'; +import type { CheckExceptionTtlActionTypes } from '../../components/expired_exceptions_list_items_modal'; +import { IncludeExpiredExceptionsModal } from '../../components/expired_exceptions_list_items_modal'; export const ListsDetailViewComponent: FC = () => { const { detailName: exceptionListId } = useParams<{ @@ -52,6 +53,7 @@ export const ListsDetailViewComponent: FC = () => { refreshExceptions, disableManageButton, onEditListDetails, + onDuplicateList, onExportList, onManageRules, onSaveManageRules, @@ -62,20 +64,33 @@ export const ListsDetailViewComponent: FC = () => { handleReferenceDelete, } = useListDetailsView(exceptionListId); - const [showExportModal, setShowExportModal] = useState(false); + const [showIncludeExpiredExceptionItemsModal, setShowIncludeExpiredExceptionItemsModal] = + useState(null); - const onModalClose = useCallback(() => setShowExportModal(false), [setShowExportModal]); + const onModalClose = useCallback( + () => setShowIncludeExpiredExceptionItemsModal(null), + [setShowIncludeExpiredExceptionItemsModal] + ); - const onModalOpen = useCallback(() => setShowExportModal(true), [setShowExportModal]); + const onModalOpen = useCallback( + (actionType: CheckExceptionTtlActionTypes) => { + setShowIncludeExpiredExceptionItemsModal(actionType); + }, + [setShowIncludeExpiredExceptionItemsModal] + ); const handleExportList = useCallback(() => { if (list?.type === ExceptionListTypeEnum.ENDPOINT) { onExportList(true); } else { - onModalOpen(); + onModalOpen('export'); } }, [onModalOpen, list, onExportList]); + const handleDuplicateList = useCallback(() => { + onModalOpen('duplicate'); + }, [onModalOpen]); + const detailsViewContent = useMemo(() => { if (viewerStatus === ViewerStatus.ERROR) return ; @@ -99,6 +114,7 @@ export const ListsDetailViewComponent: FC = () => { onExportList={handleExportList} onDeleteList={handleDelete} onManageRules={onManageRules} + onDuplicateList={handleDuplicateList} dataTestSubj="exceptionListManagement" /> @@ -125,47 +141,52 @@ export const ListsDetailViewComponent: FC = () => { onRuleSelectionChange={onRuleSelectionChange} /> ) : null} - {showExportModal && ( - )} ); }, [ - canUserEditList, - disableManageButton, - exportedList, - handleOnDownload, - headerBackOptions, - invalidListId, - isLoading, + viewerStatus, isReadOnly, - linkedRules, + isLoading, + invalidListId, + listName, list, listDescription, listId, - listName, + linkedRules, + canUserEditList, + headerBackOptions, + onEditListDetails, + handleExportList, + handleDelete, + onManageRules, + handleDuplicateList, + exportedList, + handleOnDownload, + refreshExceptions, referenceModalState.contentText, referenceModalState.rulesReferences, - refreshExceptions, - showManageButtonLoader, - showManageRulesFlyout, + handleCloseReferenceErrorModal, + handleReferenceDelete, showReferenceErrorModal, - showExportModal, - viewerStatus, + showManageRulesFlyout, + showManageButtonLoader, + disableManageButton, + onSaveManageRules, onCancelManageRules, - onEditListDetails, - onExportList, - onManageRules, onRuleSelectionChange, - onSaveManageRules, - handleCloseReferenceErrorModal, - handleDelete, - handleReferenceDelete, + showIncludeExpiredExceptionItemsModal, + onExportList, + onDuplicateList, onModalClose, - handleExportList, ]); return ( <> diff --git a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx index 4fe0596941294..22b41e88bfb85 100644 --- a/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx +++ b/x-pack/plugins/security_solution/public/exceptions/pages/shared_lists/index.tsx @@ -93,7 +93,7 @@ export const SharedLists = React.memo(() => { application: { navigateToApp }, }, } = useKibana(); - const { exportExceptionList, deleteExceptionList } = useApi(http); + const { exportExceptionList, deleteExceptionList, duplicateExceptionList } = useApi(http); const [showReferenceErrorModal, setShowReferenceErrorModal] = useState(false); const [referenceModalState, setReferenceModalState] = useState( @@ -262,6 +262,45 @@ export const SharedLists = React.memo(() => { [] ); + const handleDuplicationError = useCallback( + (err: Error) => { + addError(err, { title: i18n.EXCEPTION_DUPLICATE_ERROR }); + }, + [addError] + ); + + const handleDuplicateSuccess = useCallback( + (name: string) => (): void => { + addSuccess(i18n.EXCEPTION_LIST_DUPLICATED_SUCCESSFULLY(name)); + handleRefresh(); + }, + [addSuccess, handleRefresh] + ); + + const handleDuplicate = useCallback( + ({ + listId, + name, + namespaceType, + includeExpiredExceptions, + }: { + listId: string; + name: string; + namespaceType: NamespaceType; + includeExpiredExceptions: boolean; + }) => + async () => { + await duplicateExceptionList({ + includeExpiredExceptions, + listId, + namespaceType, + onError: handleDuplicationError, + onSuccess: handleDuplicateSuccess(name), + }); + }, + [duplicateExceptionList, handleDuplicateSuccess, handleDuplicationError] + ); + const handleCloseReferenceErrorModal = useCallback((): void => { setShowReferenceErrorModal(false); setReferenceModalState({ @@ -546,6 +585,7 @@ export const SharedLists = React.memo(() => { exceptionsList={excList} handleDelete={handleDelete} handleExport={handleExport} + handleDuplicate={handleDuplicate} /> ))} diff --git a/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts b/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts index 6d1f8c115fab3..e2688d804b3d2 100644 --- a/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts +++ b/x-pack/plugins/security_solution/public/exceptions/translations/list_details_view.ts @@ -167,3 +167,17 @@ export const EXCEPTION_EXPORT_ERROR_DESCRIPTION = i18n.translate( defaultMessage: 'An error occurred exporting a list', } ); + +export const DUPLICATE_EXCEPTION_LIST = i18n.translate( + 'xpack.securitySolution.exceptionsTable.duplicateExceptionList', + { + defaultMessage: 'Duplicate exception list', + } +); + +export const EXCEPTION_DUPLICATE_ERROR_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptionsTable.duplicateListDescription', + { + defaultMessage: 'An error occurred duplicating a list', + } +); diff --git a/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts b/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts index 6ab1ca6df8464..eb18283577369 100644 --- a/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts +++ b/x-pack/plugins/security_solution/public/exceptions/translations/shared_list.ts @@ -125,6 +125,19 @@ export const EXCEPTION_EXPORT_ERROR = i18n.translate( } ); +export const EXCEPTION_LIST_DUPLICATED_SUCCESSFULLY = (listName: string) => + i18n.translate('xpack.securitySolution.exceptions.list.duplicate_success', { + values: { listName }, + defaultMessage: 'Exception list "{listName}" duplicated successfully', + }); + +export const EXCEPTION_DUPLICATE_ERROR = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.all.exceptions.duplicateError', + { + defaultMessage: 'Exception list duplication error', + } +); + export const EXCEPTION_DELETE_ERROR = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.all.exceptions.deleteError', { @@ -371,29 +384,59 @@ export const SORT_BY_CREATE_AT = i18n.translate( } ); -export const EXPORT_MODAL_CANCEL_BUTTON = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalCancelButton', +export const EXPIRED_EXCEPTIONS_MODAL_CANCEL_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalCancelButton', { defaultMessage: 'Cancel', } ); -export const EXPORT_MODAL_TITLE = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalTitle', +export const EXPIRED_EXCEPTIONS_MODAL_EXPORT_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalExportTitle', { - defaultMessage: 'Export exception list', + defaultMessage: 'Export exception list?', } ); -export const EXPORT_MODAL_INCLUDE_SWITCH_LABEL = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalIncludeSwitchLabel', +export const EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_TITLE = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalDuplicateTitle', + { + defaultMessage: 'Duplicate exception list?', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_DUPLICATE_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalIncludeDuplicateDescription', + { + defaultMessage: + 'You’re duplicating an exception list. Switch the toggle off to exclude expired exceptions.', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_EXPORT_DESCRIPTION = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalIncludeExportDescription', + { + defaultMessage: + 'You’re exporting an exception list. Switch the toggle off to exclude expired exceptions.', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_INCLUDE_SWITCH_LABEL = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalIncludeSwitchLabel', { defaultMessage: 'Include expired exceptions', } ); -export const EXPORT_MODAL_CONFIRM_BUTTON = i18n.translate( - 'xpack.securitySolution.exceptions.exportModalConfirmButton', +export const EXPIRED_EXCEPTIONS_MODAL_CONFIRM_DUPLICATE_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalConfirmDuplicateButton', + { + defaultMessage: 'Duplicate', + } +); + +export const EXPIRED_EXCEPTIONS_MODAL_CONFIRM_EXPORT_BUTTON = i18n.translate( + 'xpack.securitySolution.exceptions.expiredExceptionModalConfirmExportButton', { defaultMessage: 'Export', } diff --git a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx index 1c95938c106ad..d9b57d3da7c29 100644 --- a/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/hosts/components/host_risk_score_table/columns.tsx @@ -38,7 +38,7 @@ export const getHostRiskScoreColumns = ({ if (hostName != null && hostName.length > 0) { return ( 0) { return ( [ return ( { title={ diff --git a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx index d8d5abb4981c0..e6af3c9a873f7 100644 --- a/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/explore/users/components/user_risk_score_table/columns.tsx @@ -41,7 +41,7 @@ export const getUserRiskScoreColumns = ({ return ( = () => { +// const contextValue = { +// getFieldsData: () => {}, +// } as unknown as LeftPanelContext; +// +// return ( +// +// +// +// ); +// }; + +export const Error: Story = () => { + const contextValue = { + getFieldsData: () => {}, + } as unknown as LeftPanelContext; + + return ( + + + + ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/session_view.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.test.tsx new file mode 100644 index 0000000000000..d334f60ba2eb3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import type { LeftPanelContext } from '../context'; +import { LeftFlyoutContext } from '../context'; +import { TestProviders } from '../../../common/mock'; +import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids'; +import { SessionView } from './session_view'; + +jest.mock('../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: jest.fn().mockReturnValue({ + services: { + sessionView: { + getSessionView: jest.fn().mockReturnValue(

), + }, + }, + }), + }; +}); + +describe('', () => { + it('renders session view correctly', () => { + const contextValue = { + getFieldsData: () => 'id', + } as unknown as LeftPanelContext; + + const wrapper = render( + + + + + + ); + expect(wrapper.getByTestId(SESSION_VIEW_TEST_ID)).toBeInTheDocument(); + }); + + it('should render error message on null eventId', () => { + const contextValue = { + getFieldsData: () => {}, + } as unknown as LeftPanelContext; + + const wrapper = render( + + + + + + ); + expect(wrapper.getByTestId(SESSION_VIEW_ERROR_TEST_ID)).toBeInTheDocument(); + expect(wrapper.getByText('Unable to display session view')).toBeInTheDocument(); + expect(wrapper.getByText('There was an error displaying session view')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx index e62745b905640..d48afe6e3f712 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/session_view.tsx @@ -7,16 +7,46 @@ import type { FC } from 'react'; import React from 'react'; -import { EuiText } from '@elastic/eui'; -import { SESSION_VIEW_TEST_ID } from './test_ids'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { getField } from '../../shared/utils'; +import { ERROR_MESSAGE, ERROR_TITLE } from '../../shared/translations'; +import { SESSION_VIEW_ERROR_MESSAGE } from './translations'; +import { SESSION_VIEW_ERROR_TEST_ID, SESSION_VIEW_TEST_ID } from './test_ids'; +import { useKibana } from '../../../common/lib/kibana'; +import { useLeftPanelContext } from '../context'; export const SESSION_VIEW_ID = 'session_view'; +const SESSION_ENTITY_ID = 'process.entry_leader.entity_id'; /** * Session view displayed in the document details expandable flyout left section under the Visualize tab */ export const SessionView: FC = () => { - return {'Session view'}; + const { sessionView } = useKibana().services; + const { getFieldsData } = useLeftPanelContext(); + + const sessionEntityId = getField(getFieldsData(SESSION_ENTITY_ID)); + + if (!sessionEntityId) { + return ( + {ERROR_TITLE(SESSION_VIEW_ERROR_MESSAGE)}} + body={

{ERROR_MESSAGE(SESSION_VIEW_ERROR_MESSAGE)}

} + data-test-subj={SESSION_VIEW_ERROR_TEST_ID} + /> + ); + } + + return ( +
+ {sessionView.getSessionView({ + sessionEntityId, + isFullScreen: true, + })} +
+ ); }; SessionView.displayName = 'SessionView'; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts index 8b2804fae3e9f..40cf67fddb180 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/test_ids.ts @@ -9,6 +9,7 @@ export const ANALYZER_GRAPH_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAnal export const ANALYZE_GRAPH_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutAnalyzerGraphError'; export const SESSION_VIEW_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionView'; +export const SESSION_VIEW_ERROR_TEST_ID = 'securitySolutionDocumentDetailsFlyoutSessionViewError'; export const ENTITIES_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesDetails'; export const THREAT_INTELLIGENCE_DETAILS_TEST_ID = 'securitySolutionDocumentDetailsFlyoutThreatIntelligenceDetails'; diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts index 8c59c8a101fb0..f82d34c859ddf 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/left/components/translations.ts @@ -13,3 +13,10 @@ export const ANALYZER_ERROR_MESSAGE = i18n.translate( defaultMessage: 'analyzer', } ); + +export const SESSION_VIEW_ERROR_MESSAGE = i18n.translate( + 'xpack.securitySolution.flyout.sessionViewErrorTitle', + { + defaultMessage: 'session view', + } +); diff --git a/x-pack/plugins/security_solution/public/flyout/left/context.tsx b/x-pack/plugins/security_solution/public/flyout/left/context.tsx index 9b564666ea487..bf6adb5cd6a07 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/context.tsx @@ -6,6 +6,16 @@ */ import React, { createContext, useContext, useMemo } from 'react'; +import { EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { SecurityPageName } from '../../../common/constants'; +import { SourcererScopeName } from '../../common/store/sourcerer/model'; +import { useSourcererDataView } from '../../common/containers/sourcerer'; +import { useTimelineEventsDetails } from '../../timelines/containers/details'; +import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; +import { useRouteSpy } from '../../common/utils/route/use_route_spy'; +import { useSpaceId } from '../../common/hooks/use_space_id'; +import { getAlertIndexAlias } from '../../timelines/components/side_panel/event_details/helpers'; import type { LeftPanelProps } from '.'; export interface LeftPanelContext { @@ -17,6 +27,10 @@ export interface LeftPanelContext { * Name of the index used in the parent's page */ indexName: string; + /** + * Retrieves searchHit values for the provided field + */ + getFieldsData: (field: string) => unknown | unknown[]; } export const LeftFlyoutContext = createContext(undefined); @@ -29,11 +43,40 @@ export type LeftPanelProviderProps = { } & Partial; export const LeftPanelProvider = ({ id, indexName, children }: LeftPanelProviderProps) => { + const currentSpaceId = useSpaceId(); + const eventIndex = indexName ? getAlertIndexAlias(indexName, currentSpaceId) ?? indexName : ''; + const [{ pageName }] = useRouteSpy(); + const sourcererScope = + pageName === SecurityPageName.detections + ? SourcererScopeName.detections + : SourcererScopeName.default; + const sourcererDataView = useSourcererDataView(sourcererScope); + const [loading, _, searchHit] = useTimelineEventsDetails({ + indexName: eventIndex, + eventId: id ?? '', + runtimeMappings: sourcererDataView.runtimeMappings, + skip: !id, + }); + const getFieldsData = useGetFieldsData(searchHit?.fields); + const contextValue = useMemo( - () => (id && indexName ? { eventId: id, indexName } : undefined), - [id, indexName] + () => (id && indexName ? { eventId: id, indexName, getFieldsData } : undefined), + [id, indexName, getFieldsData] ); + if (loading) { + return ( + + + + ); + } + return {children}; }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx index 52a85b66a3108..0045f30cecebc 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.test.tsx @@ -54,7 +54,7 @@ describe('', () => { browserFields: {}, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( @@ -62,11 +62,7 @@ describe('', () => { ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if browserFields is null', () => { @@ -78,7 +74,7 @@ describe('', () => { browserFields: null, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( @@ -86,11 +82,7 @@ describe('', () => { ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if eventId is null', () => { @@ -102,7 +94,7 @@ describe('', () => { browserFields: {}, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( @@ -110,10 +102,6 @@ describe('', () => { ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx index af14a20788466..eaae42100ded3 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/highlighted_fields.tsx @@ -33,7 +33,7 @@ export const HighlightedFields: FC = () => { }, [eventId, indexName, openRightPanel, scopeId]); if (!dataFormattedForFieldBrowser || !browserFields || !eventId) { - return <>; + return null; } return ( diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx index 8409676b610b0..9efb979ba7a26 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_section.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; import { INSIGHTS_TEST_ID } from './test_ids'; import { INSIGHTS_TITLE } from './translations'; import { EntitiesOverview } from './entities_overview'; @@ -25,6 +26,7 @@ export const InsightsSection: React.FC = ({ expanded = fal return ( + ); }; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx new file mode 100644 index 0000000000000..c1fc9dfe8a7f8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.stories.tsx @@ -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 React from 'react'; +import type { Story } from '@storybook/react'; +import { InsightsSubSection } from './insights_subsection'; + +export default { + component: InsightsSubSection, + title: 'Flyout/InsightsSubSection', +}; + +const title = 'Title'; +const children =
{'hello'}
; + +export const Basic: Story = () => { + return {children}; +}; + +export const Loading: Story = () => { + return ( + + {null} + + ); +}; + +export const NoTitle: Story = () => { + return {children}; +}; + +export const NoChildren: Story = () => { + return {null}; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx new file mode 100644 index 0000000000000..271953c8e8105 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.test.tsx @@ -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 React from 'react'; +import { render } from '@testing-library/react'; +import { InsightsSubSection } from './insights_subsection'; + +const title = 'Title'; +const dataTestSubj = 'test'; +const children =
{'hello'}
; + +describe('', () => { + it('should render children component', () => { + const { getByTestId } = render( + + {children} + + ); + + const titleDataTestSubj = `${dataTestSubj}Title`; + const contentDataTestSubj = `${dataTestSubj}Content`; + + expect(getByTestId(titleDataTestSubj)).toHaveTextContent(title); + expect(getByTestId(contentDataTestSubj)).toBeInTheDocument(); + }); + + it('should render loading component', () => { + const { getByTestId } = render( + + {children} + + ); + + const loadingDataTestSubj = `${dataTestSubj}Loading`; + expect(getByTestId(loadingDataTestSubj)).toBeInTheDocument(); + }); + + it('should render null if error', () => { + const { container } = render( + + {children} + + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if no title', () => { + const { container } = render({children}); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null if no children', () => { + const { container } = render( + + {null} + + ); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx new file mode 100644 index 0000000000000..4b5c1a541e316 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_subsection.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui'; + +export interface InsightsSectionProps { + /** + * Renders a loading spinner if true + */ + loading?: boolean; + /** + * Returns a null component if true + */ + error?: boolean; + /** + * Title at the top of the component + */ + title: string; + /** + * Content of the component + */ + children: React.ReactNode; + /** + * Prefix data-test-subj to use for the elements + */ + ['data-test-subj']?: string; +} + +/** + * Presentational component to handle loading and error in the subsections of the Insights section. + * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results + */ +export const InsightsSubSection: React.FC = ({ + loading = false, + error = false, + title, + 'data-test-subj': dataTestSubj, + children, +}) => { + const loadingDataTestSubj = `${dataTestSubj}Loading`; + // showing the loading in this component instead of SummaryPanel because we're hiding the entire section if no data + + if (loading) { + return ( + + + + + + ); + } + + // hide everything + if (error || !title || !children) { + return null; + } + + const titleDataTestSubj = `${dataTestSubj}Title`; + const contentDataTestSubj = `${dataTestSubj}Content`; + + return ( + <> + +
{title}
+
+ + + {children} + + + ); +}; + +InsightsSubSection.displayName = 'InsightsSubSection'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx new file mode 100644 index 0000000000000..5637d3c036860 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.stories.tsx @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { Story } from '@storybook/react'; +import { css } from '@emotion/react'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; + +export default { + component: InsightsSummaryPanel, + title: 'Flyout/InsightsSummaryPanel', +}; + +export const Default: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + { + icon: 'warning', + value: 2, + text: 'this is test for orange', + color: 'rgb(255,126,98)', + }, + { + icon: 'warning', + value: 3, + text: 'this is test for yellow', + color: 'rgb(241,216,11)', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const InvalidColor: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for an invalid color (abc)', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const NoColor: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + }, + { + icon: 'warning', + value: 2, + text: 'this is test for orange', + }, + { + icon: 'warning', + value: 3, + text: 'this is test for yellow', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const LongText: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is an extremely long text to verify it is properly cut off and and we show three dots at the end', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; +export const LongNumber: Story = () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is an extremely long value to verify it is properly cut off and and we show three dots at the end', + color: 'abc', + }, + ]; + + return ( +
+ +
+ ); +}; + +export const NoData: Story = () => { + const data: InsightsSummaryPanelData[] = []; + + return ( +
+ +
+ ); +}; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx new file mode 100644 index 0000000000000..9ecbbcc7fc0a5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.test.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render } from '@testing-library/react'; +import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; +import { + INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID, +} from './test_ids'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; + +describe('', () => { + it('should render by default', () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 1, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + ]; + + const { getByTestId } = render( + + + + ); + + const iconTestId = `${INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID}0`; + const valueTestId = `${INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID}0`; + const colorTestId = `${INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID}0`; + expect(getByTestId(iconTestId)).toBeInTheDocument(); + expect(getByTestId(valueTestId)).toHaveTextContent('1 this is a test for red'); + expect(getByTestId(colorTestId)).toBeInTheDocument(); + }); + + it('should only render null when data is null', () => { + const data = null as unknown as InsightsSummaryPanelData[]; + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should handle big number in a compact notation', () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is a test for red', + color: 'rgb(189,39,30)', + }, + ]; + + const { getByTestId } = render( + + + + ); + + const valueTestId = `${INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID}0`; + expect(getByTestId(valueTestId)).toHaveTextContent('160k this is a test for red'); + }); + + it(`should not show the colored dot if color isn't provided`, () => { + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: 160000, + text: 'this is a test for no color', + }, + ]; + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId(INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID)).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx new file mode 100644 index 0000000000000..306eaa101b804 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/insights_summary_panel.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { VFC } from 'react'; +import React from 'react'; +import { css } from '@emotion/react'; +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiHealth, EuiPanel } from '@elastic/eui'; +import { FormattedCount } from '../../../common/components/formatted_number'; + +export interface InsightsSummaryPanelData { + /** + * Icon to display on the left side of each row + */ + icon: string; + /** + * Number of results/entries found + */ + value: number; + /** + * Text corresponding of the number of results/entries + */ + text: string; + /** + * Optional parameter for now, will be used to display a dot on the right side + * (corresponding to some sort of severity?) + */ + color?: string; // TODO remove optional when we have guidance on what the colors will actually be +} + +export interface InsightsSummaryPanelProps { + /** + * Array of data to display in each row + */ + data: InsightsSummaryPanelData[]; + /** + * Prefix data-test-subj because this component will be used in multiple places + */ + ['data-test-subj']?: string; +} + +/** + * Panel showing summary information as an icon, a count and text as well as a severity colored dot. + * Should be used for Entities, Threat Intelligence, Prevalence, Correlations and Results components under the Insights section. + * The colored dot is currently optional but will ultimately be mandatory (waiting on PM and UIUX). + */ +export const InsightsSummaryPanel: VFC = ({ + data, + 'data-test-subj': dataTestSubj, +}) => { + if (!data || data.length === 0) { + return null; + } + + const iconDataTestSubj = `${dataTestSubj}Icon`; + const valueDataTestSubj = `${dataTestSubj}Value`; + const colorDataTestSubj = `${dataTestSubj}Color`; + + return ( + + + {data.map((row, index) => ( + + + + + + {row.text} + + {row.color && ( + + + + )} + + ))} + + + ); +}; + +InsightsSummaryPanel.displayName = 'InsightsSummaryPanel'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx index f0bc9bd993f66..24e52e4ba5915 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/mitre_attack.test.tsx @@ -33,16 +33,12 @@ describe('', () => { }, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx index d2a7c90011d68..b7050d1df0fa0 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/reason.test.tsx @@ -37,17 +37,13 @@ describe('', () => { dataAsNestedObject: {}, } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render null if dataAsNestedObject is null', () => { @@ -55,17 +51,13 @@ describe('', () => { dataFormattedForFieldBrowser: [], } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render null if renderer is null', () => { const panelContextValue = { @@ -73,16 +65,12 @@ describe('', () => { dataFormattedForFieldBrowser: [], } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx index 3b9364df1dd04..554b6c90db32a 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/risk_score.test.tsx @@ -38,17 +38,13 @@ describe('', () => { getFieldsData: jest.fn(), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if getFieldsData is invalid', () => { @@ -56,16 +52,12 @@ describe('', () => { getFieldsData: jest.fn().mockImplementation(() => 123), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx index e329459c6cbc1..92c8b0a382638 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/severity.test.tsx @@ -38,17 +38,13 @@ describe('', () => { getFieldsData: jest.fn(), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if getFieldsData is invalid array', () => { @@ -56,17 +52,13 @@ describe('', () => { getFieldsData: jest.fn().mockImplementation(() => ['abc']), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); it('should render empty component if getFieldsData is invalid string', () => { @@ -74,16 +66,12 @@ describe('', () => { getFieldsData: jest.fn().mockImplementation(() => 'abc'), } as unknown as RightPanelContext; - const { baseElement } = render( + const { container } = render( ); - expect(baseElement).toMatchInlineSnapshot(` - -
- - `); + expect(container).toBeEmptyDOMElement(); }); }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts index e89c16b3f5f12..9ee38ba2940cb 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/test_ids.ts @@ -84,6 +84,18 @@ export const ENTITIES_HOST_OVERVIEW_IP_TEST_ID = export const ENTITIES_HOST_OVERVIEW_RISK_LEVEL_TEST_ID = 'securitySolutionDocumentDetailsFlyoutEntitiesHostOverviewRiskLevel'; +/* Insights Threat Intelligence */ + +export const INSIGHTS_THREAT_INTELLIGENCE_TEST_ID = + 'securitySolutionDocumentDetailsFlyoutInsightsThreatIntelligence'; +export const INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Title`; +export const INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Content`; +export const INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}ViewAllButton`; +export const INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Loading`; +export const INSIGHTS_THREAT_INTELLIGENCE_ICON_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Icon`; +export const INSIGHTS_THREAT_INTELLIGENCE_VALUE_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Value`; +export const INSIGHTS_THREAT_INTELLIGENCE_COLOR_TEST_ID = `${INSIGHTS_THREAT_INTELLIGENCE_TEST_ID}Color`; + /* Visualizations section*/ export const VISUALIZATIONS_SECTION_TEST_ID = 'securitySolutionDocumentDetailsVisualizationsTitle'; export const VISUALIZATIONS_SECTION_HEADER_TEST_ID = diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx new file mode 100644 index 0000000000000..ecf17f2c7e822 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.test.tsx @@ -0,0 +1,195 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 } from '@testing-library/react'; +import { ExpandableFlyoutContext } from '@kbn/expandable-flyout/src/context'; +import { RightPanelContext } from '../context'; +import { + INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID, + INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID, +} from './test_ids'; +import { TestProviders } from '../../../common/mock'; +import { ThreatIntelligenceOverview } from './threat_intelligence_overview'; +import { LeftPanelInsightsTabPath, LeftPanelKey } from '../../left'; +import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; + +jest.mock('../hooks/use_fetch_threat_intelligence'); + +const panelContextValue = { + eventId: 'event id', + indexName: 'indexName', + dataFormattedForFieldBrowser: [], +} as unknown as RightPanelContext; + +const renderThreatIntelligenceOverview = (contextValue: RightPanelContext) => ( + + + + + +); + +describe('', () => { + it('should render 1 match detected and 1 field enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 1, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + 'Threat Intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '1 threat match detected' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '1 field enriched with threat intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should render 2 matches detected and 2 fields enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 2, + threatEnrichmentsCount: 2, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_TITLE_TEST_ID)).toHaveTextContent( + 'Threat Intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '2 threat matches detected' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '2 fields enriched with threat intelligence' + ); + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID)).toBeInTheDocument(); + }); + + it('should render 0 field enriched', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 0, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '0 field enriched with threat intelligence' + ); + }); + + it('should render 0 match detected', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 0, + threatEnrichmentsCount: 2, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_CONTENT_TEST_ID)).toHaveTextContent( + '0 threat match detected' + ); + }); + + it('should render loading', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: true, + }); + + const { getByTestId } = render(renderThreatIntelligenceOverview(panelContextValue)); + + expect(getByTestId(INSIGHTS_THREAT_INTELLIGENCE_LOADING_TEST_ID)).toBeInTheDocument(); + }); + + it('should render null when eventId is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + }); + const contextValue = { + ...panelContextValue, + eventId: null, + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null when dataFormattedForFieldBrowser is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + error: true, + }); + const contextValue = { + ...panelContextValue, + dataFormattedForFieldBrowser: null, + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should render null when no enrichment found is null', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 0, + threatEnrichmentsCount: 0, + }); + const contextValue = { + ...panelContextValue, + dataFormattedForFieldBrowser: [], + } as unknown as RightPanelContext; + + const { container } = render(renderThreatIntelligenceOverview(contextValue)); + + expect(container).toBeEmptyDOMElement(); + }); + + it('should navigate to left section Insights tab when clicking on button', () => { + (useFetchThreatIntelligence as jest.Mock).mockReturnValue({ + loading: false, + threatMatchesCount: 1, + threatEnrichmentsCount: 1, + }); + const flyoutContextValue = { + openLeftPanel: jest.fn(), + } as unknown as ExpandableFlyoutContext; + + const { getByTestId } = render( + + + + + + + + ); + + getByTestId(INSIGHTS_THREAT_INTELLIGENCE_VIEW_ALL_BUTTON_TEST_ID).click(); + expect(flyoutContextValue.openLeftPanel).toHaveBeenCalledWith({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: panelContextValue.eventId, + indexName: panelContextValue.indexName, + }, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx new file mode 100644 index 0000000000000..63f0862a68b3a --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/components/threat_intelligence_overview.tsx @@ -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 React, { useCallback } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { useExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useFetchThreatIntelligence } from '../hooks/use_fetch_threat_intelligence'; +import { InsightsSubSection } from './insights_subsection'; +import type { InsightsSummaryPanelData } from './insights_summary_panel'; +import { InsightsSummaryPanel } from './insights_summary_panel'; +import { useRightPanelContext } from '../context'; +import { INSIGHTS_THREAT_INTELLIGENCE_TEST_ID } from './test_ids'; +import { + VIEW_ALL, + THREAT_INTELLIGENCE_TITLE, + THREAT_INTELLIGENCE_TEXT, + THREAT_MATCH_DETECTED, + THREAT_ENRICHMENT, + THREAT_MATCHES_DETECTED, + THREAT_ENRICHMENTS, +} from './translations'; +import { LeftPanelKey, LeftPanelInsightsTabPath } from '../../left'; + +/** + * Threat Intelligence section under Insights section, overview tab. + * The component fetches the necessary data, then pass it down to the InsightsSubSection component for loading and error state, + * and the SummaryPanel component for data rendering. + */ +export const ThreatIntelligenceOverview: React.FC = () => { + const { eventId, indexName, dataFormattedForFieldBrowser } = useRightPanelContext(); + const { openLeftPanel } = useExpandableFlyoutContext(); + + const goToThreatIntelligenceTab = useCallback(() => { + openLeftPanel({ + id: LeftPanelKey, + path: LeftPanelInsightsTabPath, + params: { + id: eventId, + indexName, + }, + }); + }, [eventId, openLeftPanel, indexName]); + + const { loading, threatMatchesCount, threatEnrichmentsCount } = useFetchThreatIntelligence({ + dataFormattedForFieldBrowser, + }); + + const data: InsightsSummaryPanelData[] = [ + { + icon: 'image', + value: threatMatchesCount, + text: threatMatchesCount <= 1 ? THREAT_MATCH_DETECTED : THREAT_MATCHES_DETECTED, + }, + { + icon: 'warning', + value: threatEnrichmentsCount, + text: threatMatchesCount <= 1 ? THREAT_ENRICHMENT : THREAT_ENRICHMENTS, + }, + ]; + + const error: boolean = + !eventId || + !dataFormattedForFieldBrowser || + (threatMatchesCount === 0 && threatEnrichmentsCount === 0); + + return ( + + + + {VIEW_ALL(THREAT_INTELLIGENCE_TEXT)} + + + ); +}; + +ThreatIntelligenceOverview.displayName = 'ThreatIntelligenceOverview'; diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts index 1a6a2da7344c4..d5b9f0d1928b3 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts +++ b/x-pack/plugins/security_solution/public/flyout/right/components/translations.ts @@ -101,11 +101,18 @@ export const HIGHLIGHTED_FIELDS_TITLE = i18n.translate( { defaultMessage: 'Highlighted fields' } ); +/* Insights section */ + export const ENTITIES_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.entitiesTitle', { defaultMessage: 'Entities' } ); +export const THREAT_INTELLIGENCE_TITLE = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.threatIntelligenceTitle', + { defaultMessage: 'Threat Intelligence' } +); + export const INSIGHTS_TITLE = i18n.translate( 'xpack.securitySolution.flyout.documentDetails.insightsTitle', { defaultMessage: 'Insights' } @@ -131,6 +138,41 @@ export const ENTITIES_TEXT = i18n.translate( } ); +export const THREAT_INTELLIGENCE_TEXT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligenceText', + { + defaultMessage: 'fields of threat intelligence', + } +); + +export const THREAT_MATCH_DETECTED = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatch', + { + defaultMessage: `threat match detected`, + } +); + +export const THREAT_MATCHES_DETECTED = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatMatches', + { + defaultMessage: `threat matches detected`, + } +); + +export const THREAT_ENRICHMENT = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichment', + { + defaultMessage: `field enriched with threat intelligence`, + } +); + +export const THREAT_ENRICHMENTS = i18n.translate( + 'xpack.securitySolution.flyout.documentDetails.overviewTab.threatIntelligence.threatEnrichments', + { + defaultMessage: `fields enriched with threat intelligence`, + } +); + export const VIEW_ALL = (text: string) => i18n.translate('xpack.securitySolution.flyout.documentDetails.overviewTab.viewAllButton', { values: { text }, diff --git a/x-pack/plugins/security_solution/public/flyout/right/context.tsx b/x-pack/plugins/security_solution/public/flyout/right/context.tsx index 282c224b5a15f..2da844946cbbd 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/context.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/context.tsx @@ -19,6 +19,7 @@ import { SecurityPageName } from '../../../common/constants'; import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import type { RightPanelProps } from '.'; +import type { GetFieldsData } from '../../common/hooks/use_get_fields_data'; import { useGetFieldsData } from '../../common/hooks/use_get_fields_data'; export interface RightPanelContext { @@ -57,7 +58,7 @@ export interface RightPanelContext { /** * Retrieves searchHit values for the provided field */ - getFieldsData: (field: string) => unknown | unknown[]; + getFieldsData: GetFieldsData; } export const RightPanelContext = createContext(undefined); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx new file mode 100644 index 0000000000000..ab57dcebe32af --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.test.tsx @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { RenderHookResult } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; +import type { + UseThreatIntelligenceParams, + UseThreatIntelligenceValue, +} from './use_fetch_threat_intelligence'; +import { useFetchThreatIntelligence } from './use_fetch_threat_intelligence'; +import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment'; + +jest.mock('../../../common/containers/cti/event_enrichment'); + +const dataFormattedForFieldBrowser = [ + { + category: 'kibana', + field: 'kibana.alert.rule.uuid', + isObjectArray: false, + originalValue: ['uuid'], + values: ['uuid'], + }, + { + category: 'threat', + field: 'threat.enrichments', + isObjectArray: true, + originalValue: ['{"indicator.file.hash.sha256":["sha256"]}'], + values: ['{"indicator.file.hash.sha256":["sha256"]}'], + }, + { + category: 'threat', + field: 'threat.enrichments.indicator.file.hash.sha256', + isObjectArray: false, + originalValue: ['sha256'], + values: ['sha256'], + }, +]; + +describe('useFetchThreatIntelligence', () => { + let hookResult: RenderHookResult; + + it('should render 1 match detected and 1 field enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 2, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(1); + expect(hookResult.result.current.threatMatchesCount).toEqual(1); + expect(hookResult.result.current.threatEnrichments).toHaveLength(1); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(1); + }); + + it('should render 2 matches detected and 2 fields enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.3'], + 'matched.type': ['indicator_match_rule'], + }, + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.4'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 4, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(2); + expect(hookResult.result.current.threatMatchesCount).toEqual(2); + expect(hookResult.result.current.threatEnrichments).toHaveLength(2); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(2); + }); + + it('should render 0 field enriched', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.1'], + 'matched.type': ['indicator_match_rule'], + }, + ], + totalCount: 1, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toHaveLength(1); + expect(hookResult.result.current.threatMatchesCount).toEqual(1); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); + + it('should render 0 match detected', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [ + { + 'threat.indicator.file.hash.sha256': 'sha256', + 'matched.atomic': ['sha256'], + 'matched.field': ['file.hash.sha256'], + 'matched.id': ['matched.id.2'], + 'matched.type': ['investigation_time'], + 'event.type': ['indicator'], + }, + ], + totalCount: 1, + }, + loading: false, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toHaveLength(1); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(1); + }); + + it('should return loading true', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: undefined, + loading: true, + }); + + hookResult = renderHook(() => useFetchThreatIntelligence({ dataFormattedForFieldBrowser })); + expect(hookResult.result.current.loading).toEqual(true); + expect(hookResult.result.current.error).toEqual(false); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); + + it('should return error true', () => { + (useInvestigationTimeEnrichment as jest.Mock).mockReturnValue({ + result: { + enrichments: [], + totalCount: 0, + }, + loading: false, + }); + + hookResult = renderHook(() => + useFetchThreatIntelligence({ dataFormattedForFieldBrowser: null }) + ); + expect(hookResult.result.current.loading).toEqual(false); + expect(hookResult.result.current.error).toEqual(true); + expect(hookResult.result.current.threatMatches).toEqual(undefined); + expect(hookResult.result.current.threatMatchesCount).toEqual(0); + expect(hookResult.result.current.threatEnrichments).toEqual(undefined); + expect(hookResult.result.current.threatEnrichmentsCount).toEqual(0); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.ts new file mode 100644 index 0000000000000..4f3d23b082664 --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_fetch_threat_intelligence.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 { useMemo } from 'react'; +import { groupBy } from 'lodash'; +import type { TimelineEventsDetailsItem } from '@kbn/timelines-plugin/common'; +import type { CtiEnrichment } from '../../../../common/search_strategy'; +import { useBasicDataFromDetailsData } from '../../../timelines/components/side_panel/event_details/helpers'; +import { + filterDuplicateEnrichments, + getEnrichmentFields, + parseExistingEnrichments, + timelineDataToEnrichment, +} from '../../../common/components/event_details/cti_details/helpers'; +import { useInvestigationTimeEnrichment } from '../../../common/containers/cti/event_enrichment'; +import { ENRICHMENT_TYPES } from '../../../../common/cti/constants'; + +export interface UseThreatIntelligenceParams { + /** + * An array of field objects with category and value + */ + dataFormattedForFieldBrowser: TimelineEventsDetailsItem[] | null; +} + +export interface UseThreatIntelligenceValue { + /** + * Returns true while the threat intelligence data is being queried + */ + loading: boolean; + /** + * Returns true if the dataFormattedForFieldBrowser property is null + */ + error: boolean; + /** + * Threat matches (from an indicator match rule) + */ + threatMatches: CtiEnrichment[]; + /** + * Threat matches count + */ + threatMatchesCount: number; + /** + * Threat enrichments (from the real time query) + */ + threatEnrichments: CtiEnrichment[]; + /** + * Threat enrichments count + */ + threatEnrichmentsCount: number; +} + +/** + * Hook to retrieve threat intelligence data for the expandable flyout right and left sections. + */ +export const useFetchThreatIntelligence = ({ + dataFormattedForFieldBrowser, +}: UseThreatIntelligenceParams): UseThreatIntelligenceValue => { + const { isAlert } = useBasicDataFromDetailsData(dataFormattedForFieldBrowser); + + // retrieve the threat enrichment fields with value for the current document + // (see https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/common/cti/constants.ts#L35) + const eventFields = useMemo( + () => getEnrichmentFields(dataFormattedForFieldBrowser || []), + [dataFormattedForFieldBrowser] + ); + + // retrieve existing enrichment fields and their value + const existingEnrichments = useMemo( + () => + isAlert + ? parseExistingEnrichments(dataFormattedForFieldBrowser || []).map((enrichmentData) => + timelineDataToEnrichment(enrichmentData) + ) + : [], + [dataFormattedForFieldBrowser, isAlert] + ); + + // api call to retrieve all documents that match the eventFields + const { result: response, loading } = useInvestigationTimeEnrichment(eventFields); + + // combine existing enrichment and enrichment from the api response + // also removes the investigation-time enrichments if the exact indicator already exists + const allEnrichments = useMemo(() => { + if (loading || !response?.enrichments) { + return existingEnrichments; + } + return filterDuplicateEnrichments([...existingEnrichments, ...response.enrichments]); + }, [loading, response, existingEnrichments]); + + // separate threat matches (from indicator-match rule) from threat enrichments (realtime query) + const { + [ENRICHMENT_TYPES.IndicatorMatchRule]: threatMatches, + [ENRICHMENT_TYPES.InvestigationTime]: threatEnrichments, + } = groupBy(allEnrichments, 'matched.type'); + + return { + loading, + error: !dataFormattedForFieldBrowser, + threatMatches, + threatMatchesCount: (threatMatches || []).length, + threatEnrichments, + threatEnrichmentsCount: (threatEnrichments || []).length, + }; +}; diff --git a/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx new file mode 100644 index 0000000000000..984b2a2e223dc --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.test.tsx @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ExpandableFlyoutApi } from '@kbn/expandable-flyout'; +import { useSyncToUrl } from '@kbn/url-state'; +import { renderHook } from '@testing-library/react-hooks'; +import { useSyncFlyoutStateWithUrl } from './use_sync_flyout_state_with_url'; + +jest.mock('@kbn/url-state'); + +describe('useSyncFlyoutStateWithUrl', () => { + it('should return an array containing flyoutApi ref and handleFlyoutChanges function', () => { + const { result } = renderHook(() => useSyncFlyoutStateWithUrl()); + const [flyoutApi, handleFlyoutChanges] = result.current; + + expect(flyoutApi.current).toBeNull(); + expect(typeof handleFlyoutChanges).toBe('function'); + }); + + it('should open flyout when relevant url state is detected in the query string', () => { + jest.useFakeTimers(); + + jest.mocked(useSyncToUrl).mockImplementation((_urlKey, callback) => { + setTimeout(() => callback({ mocked: { flyout: 'state' } }), 0); + return jest.fn(); + }); + + const { result } = renderHook(() => useSyncFlyoutStateWithUrl()); + const [flyoutApi, handleFlyoutChanges] = result.current; + + const flyoutApiMock: ExpandableFlyoutApi = { + openFlyout: jest.fn(), + getState: () => ({ left: undefined, right: undefined, preview: [] }), + }; + + expect(typeof handleFlyoutChanges).toBe('function'); + expect(flyoutApi.current).toBeNull(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (flyoutApi as any).current = flyoutApiMock; + + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + + expect(flyoutApiMock.openFlyout).toHaveBeenCalledTimes(1); + expect(flyoutApiMock.openFlyout).toHaveBeenCalledWith({ mocked: { flyout: 'state' } }); + }); + + it('should sync flyout state to url whenever handleFlyoutChanges is called by the consumer', () => { + const syncStateToUrl = jest.fn(); + jest.mocked(useSyncToUrl).mockImplementation((_urlKey, callback) => { + setTimeout(() => callback({ mocked: { flyout: 'state' } }), 0); + return syncStateToUrl; + }); + + const { result } = renderHook(() => useSyncFlyoutStateWithUrl()); + const [_flyoutApi, handleFlyoutChanges] = result.current; + + handleFlyoutChanges(); + + expect(syncStateToUrl).toHaveBeenCalledTimes(1); + expect(syncStateToUrl).toHaveBeenLastCalledWith(undefined); + + handleFlyoutChanges({ left: undefined, right: undefined, preview: [] }); + + expect(syncStateToUrl).toHaveBeenLastCalledWith({ + left: undefined, + right: undefined, + preview: undefined, + }); + expect(syncStateToUrl).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx new file mode 100644 index 0000000000000..be1b28147f63d --- /dev/null +++ b/x-pack/plugins/security_solution/public/flyout/url/use_sync_flyout_state_with_url.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useRef } from 'react'; +import type { ExpandableFlyoutApi, ExpandableFlyoutContext } from '@kbn/expandable-flyout'; +import { useSyncToUrl } from '@kbn/url-state'; +import last from 'lodash/last'; + +const URL_KEY = 'eventFlyout' as const; + +type FlyoutState = Parameters[0]; + +/** + * Sync flyout state with the url and open it when relevant url state is detected in the query string + * @returns [ref, flyoutChangesHandler] + */ +export const useSyncFlyoutStateWithUrl = () => { + const flyoutApi = useRef(null); + + const syncStateToUrl = useSyncToUrl(URL_KEY, (data) => { + flyoutApi.current?.openFlyout(data); + }); + + // This should be bound to flyout changed and closed events. + // When flyout is closed, url state is cleared + const handleFlyoutChanges = useCallback( + (state?: ExpandableFlyoutContext['panels']) => { + if (!state) { + return syncStateToUrl(undefined); + } + + return syncStateToUrl({ + ...state, + preview: last(state.preview), + }); + }, + [syncStateToUrl] + ); + + return [flyoutApi, handleFlyoutChanges] as const; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx index c3f94871c8f67..8d5d8907924f6 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/command_render_components/integration_tests/execute_action.test.tsx @@ -23,6 +23,8 @@ import { getEndpointAuthzInitialStateMock } from '../../../../../../common/endpo import type { EndpointPrivileges } from '../../../../../../common/endpoint/types'; import { INSUFFICIENT_PRIVILEGES_FOR_COMMAND } from '../../../../../common/translations'; import type { HttpFetchOptionsWithPath } from '@kbn/core-http-browser'; +import { endpointActionResponseCodes } from '../../lib/endpoint_action_response_codes'; +import { EndpointActionGenerator } from '../../../../../../common/endpoint/data_generators/endpoint_action_generator'; jest.mock('../../../../../common/components/user_privileges'); jest.mock('../../../../../common/experimental_features_service'); @@ -180,4 +182,36 @@ describe('When using execute action from response actions console', () => { ); }); }); + + it.each( + Object.keys(endpointActionResponseCodes).filter((key) => key.startsWith('ra_execute_error')) + )('should display known error message for response failure: %s', async (errorCode) => { + apiMocks.responseProvider.actionDetails.mockReturnValue({ + data: new EndpointActionGenerator('seed').generateActionDetails({ + command: 'execute', + errors: ['some error happen in endpoint'], + wasSuccessful: false, + outputs: { + 'agent-a': { + content: { + code: errorCode, + }, + }, + }, + }), + }); + + const { getByTestId } = await render(); + enterConsoleCommand(renderResult, 'execute --command="ls -l"'); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalled(); + }); + + await waitFor(() => { + expect(getByTestId('execute-actionFailure')).toHaveTextContent( + endpointActionResponseCodes[errorCode] + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts index 746b9201cd200..c2595c625b7fa 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_responder/lib/endpoint_action_response_codes.ts @@ -123,6 +123,97 @@ const CODES = Object.freeze({ 'xpack.securitySolution.endpointActionResponseCodes.killProcess.notPermittedSuccess', { defaultMessage: 'The provided process cannot be killed' } ), + + // ----------------------------------------------------------------- + // EXECUTE CODES + // ----------------------------------------------------------------- + + // Dev: + // Something interrupted preparing the zip: file read error, zip error. I think these should be rare, + // and should succeed on retry by the user or result in file-not-found. We might implement some retries + // internally but I'm leaning to the opinion that we should rather quickly send the feedback to the + // user to let them decide. + ra_execute_error_processing: i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingError', + { + defaultMessage: 'Unable to create execution output zip file.', + } + ), + + // Dev: + // Executing timeout has been reached, the command was killed. + 'ra_execute_error_processing-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingTimeout', + { defaultMessage: 'Command execution was terminated. It exceeded the provided timeout.' } + ), + + // Dev: + // Execution was interrupted, for example: system shutdown, endpoint service stop/restart. + 'ra_execute_error_processing-interrupted': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.processingInterrupted', + { + defaultMessage: 'Command execution was absolutely interrupted.', + } + ), + + // Dev: + // Too many active execute actions, limit 10. Execute actions are allowed to run in parallel, we must + // take into account resource use impact on endpoint as customers are piky about CPU/MEM utilization. + 'ra_execute_error_to-many-requests': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.toManyRequests', + { + defaultMessage: 'Too many concurrent command execution actions.', + } + ), + + // Dev: + // generic failure (rare corner case, software bug, etc) + ra_execute_error_failure: i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.failure', + { defaultMessage: 'Unknown failure while executing command.' } + ), + + // Dev: + // Max pending response zip uploads has been reached, limit 10. Endpoint can't use unlimited disk space. + 'ra_execute_error_disk-quota': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.diskQuotaError', + { + defaultMessage: 'Too many pending command execution output zip files.', + } + ), + + // Dev: + // The fleet upload API was unreachable (not just busy). This may mean policy misconfiguration, in which + // case health status in Kibana should indicate degraded, or maybe network configuration problems, or fleet + // server problems HTTP 500. This excludes offline status, where endpoint should just wait for network connection. + 'ra_execute_error_upload-api-unreachable': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.uploadApiUnreachable', + { + defaultMessage: + 'Failed to upload command execution output zip file. Unable to reach Fleet Server upload API.', + } + ), + + // Dev: + // Perhaps internet connection was too slow or unstable to upload all chunks before unique + // upload-id expired. Endpoint will re-try a bit, max 3 times. + 'ra_execute_error_upload-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.outputUploadTimeout', + { + defaultMessage: 'Failed to upload command execution output zip file. Upload timed out', + } + ), + + // DEV: + // Upload API could be busy, endpoint should periodically re-try (2 days = 192 x 15min, assuming + // that with 1Mbps 15min is enough to upload 100MB) + 'ra_execute_error_queue-timeout': i18n.translate( + 'xpack.securitySolution.endpointActionResponseCodes.execute.queueTimeout', + { + defaultMessage: + 'Failed to upload command execution output zip file. Timed out while queued waiting for Fleet Server', + } + ), }); /** diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx index 6360b55b1c06f..6fc62dd6cf851 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_response_actions_list/integration_tests/response_actions_log.test.tsx @@ -652,10 +652,14 @@ describe('Response actions history', () => { }); it('should contain expected output accordions for `execute` action WITH execute operation privilege', async () => { - const actionDetails = await getActionListMock({ actionCount: 1, commands: ['execute'] }); + const actionListApiResponse = await getActionListMock({ + actionCount: 1, + agentIds: ['agent-a'], + commands: ['execute'], + }); useGetEndpointActionListMock.mockReturnValue({ ...getBaseMockedActionList(), - data: actionDetails, + data: actionListApiResponse, }); mockUseGetFileInfo = { @@ -669,17 +673,7 @@ describe('Response actions history', () => { isFetched: true, error: null, data: { - data: { - ...apiMocks.responseProvider.actionDetails({ - path: `/api/endpoint/action/${actionDetails.data[0].id}`, - }).data, - outputs: { - [actionDetails.data[0].agents[0]]: { - content: {}, - type: 'json', - }, - }, - }, + data: actionListApiResponse.data[0], }, }; @@ -714,7 +708,11 @@ describe('Response actions history', () => { }); useGetEndpointActionListMock.mockReturnValue({ ...getBaseMockedActionList(), - data: await getActionListMock({ actionCount: 1, commands: ['execute'] }), + data: await getActionListMock({ + actionCount: 1, + commands: ['execute'], + agentIds: ['agent-a'], + }), }); render(); @@ -723,10 +721,7 @@ describe('Response actions history', () => { const expandButton = getByTestId(`${testPrefix}-expand-button`); userEvent.click(expandButton); - const executeAccordions = getByTestId( - `${testPrefix}-actionsLogTray-executeResponseOutput-output` - ); - expect(executeAccordions).toBeTruthy(); + expect(getByTestId(`${testPrefix}-actionsLogTray-executeResponseOutput-output`)); }); it('should not contain full output download link in expanded row for `execute` action WITHOUT Actions Log privileges', async () => { diff --git a/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts b/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts index a48498a7ee43b..c461f712b75b5 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/cypress.d.ts @@ -11,8 +11,11 @@ import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; import type { IndexedEndpointPolicyResponse } from '../../../common/endpoint/data_loaders/index_endpoint_policy_response'; -import type { HostPolicyResponse } from '../../../common/endpoint/types'; -import type { IndexEndpointHostsCyTaskOptions } from './types'; +import type { + HostPolicyResponse, + LogsEndpointActionResponse, +} from '../../../common/endpoint/types'; +import type { IndexEndpointHostsCyTaskOptions, HostActionResponse } from './types'; import type { DeleteIndexedFleetEndpointPoliciesResponse, IndexedFleetEndpointPolicyResponse, @@ -115,6 +118,12 @@ declare global { arg: IndexedEndpointPolicyResponse, options?: Partial ): Chainable; + + task( + name: 'sendHostActionResponse', + arg: HostActionResponse, + options?: Partial + ): Chainable; } } } diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts index 35b931675a6c2..d5c946af96c14 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/endpoints.cy.ts @@ -5,18 +5,31 @@ * 2.0. */ +import type { ReturnTypeFromChainable } from '../../types'; +import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; import { login } from '../../tasks/login'; -import { runEndpointLoaderScript } from '../../tasks/run_endpoint_loader'; describe('Endpoints page', () => { + let endpointData: ReturnTypeFromChainable; + before(() => { - runEndpointLoaderScript(); + indexEndpointHosts().then((indexEndpoints) => { + endpointData = indexEndpoints; + }); }); beforeEach(() => { login(); }); + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + }); + it('Loads the endpoints page', () => { cy.visit('/app/security/administration/endpoints'); cy.contains('Hosts running Elastic Defend').should('exist'); diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts new file mode 100644 index 0000000000000..579c0cab8c540 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/mocked_data/isolate.cy.ts @@ -0,0 +1,328 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getEndpointListPath } from '../../../common/routing'; +import { + interceptActionRequests, + isolateHostWithComment, + openAlertDetails, + openCaseAlertDetails, + releaseHostWithComment, + sendActionResponse, + waitForReleaseOption, +} from '../../tasks/isolate'; +import type { ActionDetails } from '../../../../../common/endpoint/types'; +import { closeAllToasts } from '../../tasks/close_all_toasts'; +import type { ReturnTypeFromChainable } from '../../types'; +import { addAlertsToCase } from '../../tasks/add_alerts_to_case'; +import { APP_ALERTS_PATH, APP_CASES_PATH, APP_PATH } from '../../../../../common/constants'; +import { login } from '../../tasks/login'; +import { indexNewCase } from '../../tasks/index_new_case'; +import { indexEndpointHosts } from '../../tasks/index_endpoint_hosts'; +import { indexEndpointRuleAlerts } from '../../tasks/index_endpoint_rule_alerts'; + +describe('Isolate command', () => { + describe('from Manage', () => { + let endpointData: ReturnTypeFromChainable; + let isolatedEndpointData: ReturnTypeFromChainable; + + before(() => { + indexEndpointHosts({ + count: 2, + withResponseActions: false, + isolation: false, + }).then((indexEndpoints) => { + endpointData = indexEndpoints; + }); + + indexEndpointHosts({ + count: 2, + withResponseActions: false, + isolation: true, + }).then((indexEndpoints) => { + isolatedEndpointData = indexEndpoints; + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (isolatedEndpointData) { + isolatedEndpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + isolatedEndpointData = undefined; + } + }); + beforeEach(() => { + login(); + }); + it('should allow filtering endpoint by Isolated status', () => { + cy.visit(APP_PATH + getEndpointListPath({ name: 'endpointList' })); + closeAllToasts(); + cy.getByTestSubj('adminSearchBar') + .click() + .type('united.endpoint.Endpoint.state.isolation: true'); + cy.getByTestSubj('querySubmitButton').click(); + cy.contains('Showing 2 endpoints'); + cy.getByTestSubj('endpointListTable').within(() => { + cy.get('tbody tr').each(($tr) => { + cy.wrap($tr).within(() => { + cy.get('td').eq(1).should('contain.text', 'Isolated'); + }); + }); + }); + }); + }); + + describe('from Alerts', () => { + let endpointData: ReturnTypeFromChainable; + let alertData: ReturnTypeFromChainable; + let hostname: string; + + before(() => { + indexEndpointHosts({ withResponseActions: false, isolation: false }) + .then((indexEndpoints) => { + endpointData = indexEndpoints; + hostname = endpointData.data.hosts[0].host.name; + }) + .then(() => { + return indexEndpointRuleAlerts({ + endpointAgentId: endpointData.data.hosts[0].agent.id, + endpointHostname: endpointData.data.hosts[0].host.name, + endpointIsolated: false, + }); + }); + }); + + after(() => { + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + // @ts-expect-error ignore setting to undefined + alertData = undefined; + } + }); + + beforeEach(() => { + login(); + }); + + it('should isolate and release host', () => { + const isolateComment = `Isolating ${hostname}`; + const releaseComment = `Releasing ${hostname}`; + let isolateRequestResponse: ActionDetails; + let releaseRequestResponse: ActionDetails; + + cy.visit(APP_ALERTS_PATH); + closeAllToasts(); + + cy.getByTestSubj('alertsTable').within(() => { + cy.getByTestSubj('expand-event') + .first() + .within(() => { + cy.get(`[data-is-loading="true"]`).should('exist'); + }); + cy.getByTestSubj('expand-event') + .first() + .within(() => { + cy.get(`[data-is-loading="true"]`).should('not.exist'); + }); + }); + + openAlertDetails(); + + isolateHostWithComment(isolateComment, hostname); + + interceptActionRequests((responseBody) => { + isolateRequestResponse = responseBody; + }, 'isolate'); + + cy.getByTestSubj('hostIsolateConfirmButton').click(); + + cy.wait('@isolate').then(() => { + sendActionResponse(isolateRequestResponse); + }); + + cy.contains(`Isolation on host ${hostname} successfully submitted`); + + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.wait(1000); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.contains('Release host').click(); + } else { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.contains('Isolated'); + }); + cy.contains('Release host').click(); + } + }); + + releaseHostWithComment(releaseComment, hostname); + + interceptActionRequests((responseBody) => { + releaseRequestResponse = responseBody; + }, 'release'); + + cy.contains('Confirm').click(); + + cy.wait('@release').then(() => { + sendActionResponse(releaseRequestResponse); + }); + + cy.contains(`Release on host ${hostname} successfully submitted`); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openAlertDetails(); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.get('[title="Isolated"]').should('not.exist'); + }); + }); + }); + + describe('from Cases', () => { + let endpointData: ReturnTypeFromChainable; + let caseData: ReturnTypeFromChainable; + let alertData: ReturnTypeFromChainable; + let caseAlertActions: ReturnType; + let alertId: string; + let caseUrlPath: string; + let hostname: string; + + before(() => { + indexNewCase().then((indexCase) => { + caseData = indexCase; + caseUrlPath = `${APP_CASES_PATH}/${indexCase.data.id}`; + }); + + indexEndpointHosts({ withResponseActions: false, isolation: false }) + .then((indexEndpoints) => { + endpointData = indexEndpoints; + hostname = endpointData.data.hosts[0].host.name; + }) + .then(() => { + return indexEndpointRuleAlerts({ + endpointAgentId: endpointData.data.hosts[0].agent.id, + endpointHostname: endpointData.data.hosts[0].host.name, + endpointIsolated: false, + }).then((indexedAlert) => { + alertData = indexedAlert; + alertId = alertData.alerts[0]._id; + }); + }) + .then(() => { + caseAlertActions = addAlertsToCase({ + caseId: caseData.data.id, + alertIds: [alertId], + }); + }); + }); + + after(() => { + if (caseData) { + caseData.cleanup(); + // @ts-expect-error ignore setting to undefined + caseData = undefined; + } + + if (endpointData) { + endpointData.cleanup(); + // @ts-expect-error ignore setting to undefined + endpointData = undefined; + } + + if (alertData) { + alertData.cleanup(); + // @ts-expect-error ignore setting to undefined + alertData = undefined; + } + }); + + beforeEach(() => { + login(); + }); + + it('should isolate and release host', () => { + let isolateRequestResponse: ActionDetails; + let releaseRequestResponse: ActionDetails; + const isolateComment = `Isolating ${hostname}`; + const releaseComment = `Releasing ${hostname}`; + const caseAlertId = caseAlertActions.comments[alertId]; + + cy.visit(caseUrlPath); + closeAllToasts(); + openCaseAlertDetails(caseAlertId); + + isolateHostWithComment(isolateComment, hostname); + + interceptActionRequests((responseBody) => { + isolateRequestResponse = responseBody; + }, 'isolate'); + + cy.getByTestSubj('hostIsolateConfirmButton').click(); + + cy.wait('@isolate').then(() => { + sendActionResponse(isolateRequestResponse); + }); + + cy.contains(`Isolation on host ${hostname} successfully submitted`); + + cy.getByTestSubj('euiFlyoutCloseButton').click(); + + cy.getByTestSubj('user-actions-list').within(() => { + cy.contains(isolateComment); + cy.get('[aria-label="lock"]').should('exist'); + cy.get('[aria-label="lockOpen"]').should('not.exist'); + }); + + waitForReleaseOption(caseAlertId); + + releaseHostWithComment(releaseComment, hostname); + + interceptActionRequests((responseBody) => { + releaseRequestResponse = responseBody; + }, 'release'); + + cy.contains('Confirm').click(); + + cy.wait('@release').then(() => { + sendActionResponse(releaseRequestResponse); + }); + + cy.contains(`Release on host ${hostname} successfully submitted`); + cy.getByTestSubj('euiFlyoutCloseButton').click(); + + cy.getByTestSubj('user-actions-list').within(() => { + cy.contains(releaseComment); + cy.contains(isolateComment); + cy.get('[aria-label="lock"]').should('exist'); + cy.get('[aria-label="lockOpen"]').should('exist'); + }); + + openCaseAlertDetails(caseAlertId); + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + cy.getByTestSubj(`comment-action-show-alert-${caseAlertId}`).click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); + } + cy.get('[title="Isolated"]').should('not.exist'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts index b31ee2ec25874..6dd4bedaa8937 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/data_loaders.ts @@ -8,12 +8,17 @@ // / import type { CasePostRequest } from '@kbn/cases-plugin/common/api'; +import { sendEndpointActionResponse } from '../../../../scripts/endpoint/agent_emulator/services/endpoint_response_actions'; import type { IndexedEndpointPolicyResponse } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; import { deleteIndexedEndpointPolicyResponse, indexEndpointPolicyResponse, } from '../../../../common/endpoint/data_loaders/index_endpoint_policy_response'; -import type { HostPolicyResponse } from '../../../../common/endpoint/types'; +import type { + ActionDetails, + HostPolicyResponse, + LogsEndpointActionResponse, +} from '../../../../common/endpoint/types'; import type { IndexEndpointHostsCyTaskOptions } from '../types'; import type { IndexedEndpointRuleAlerts, @@ -95,12 +100,14 @@ export const dataLoaders = ( indexEndpointHosts: async (options: IndexEndpointHostsCyTaskOptions = {}) => { const { kbnClient, esClient } = await stackServicesPromise; - const { count: numHosts, version, os } = options; + const { count: numHosts, version, os, isolation, withResponseActions } = options; return cyLoadEndpointDataHandler(esClient, kbnClient, { numHosts, version, os, + isolation, + withResponseActions, }); }, @@ -140,5 +147,13 @@ export const dataLoaders = ( const { esClient } = await stackServicesPromise; return deleteIndexedEndpointPolicyResponse(esClient, indexedData).then(() => null); }, + + sendHostActionResponse: async (data: { + action: ActionDetails; + state: { state?: 'success' | 'failure' }; + }): Promise => { + const { esClient } = await stackServicesPromise; + return sendEndpointActionResponse(esClient, data.action, { state: data.state.state }); + }, }); }; diff --git a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts index cfff2e9a2a4b0..9d0f5ac135d5d 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/support/plugin_handlers/endpoint_data_loader.ts @@ -36,6 +36,9 @@ export interface CyLoadEndpointDataOptions enableFleetIntegration: boolean; generatorSeed: string; waitUntilTransformed: boolean; + withResponseActions: boolean; + isolation: boolean; + bothIsolatedAndNormalEndpoints?: boolean; } /** @@ -58,10 +61,12 @@ export const cyLoadEndpointDataHandler = async ( waitUntilTransformed = true, version = kibanaPackageJson.version, os, + withResponseActions, + isolation, } = options; const DocGenerator = EndpointDocGenerator.custom({ - CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os }), + CustomMetadataGenerator: EndpointMetadataGenerator.custom({ version, os, isolation }), }); if (waitUntilTransformed) { @@ -85,7 +90,8 @@ export const cyLoadEndpointDataHandler = async ( alertsPerHost, enableFleetIntegration, undefined, - DocGenerator + DocGenerator, + withResponseActions ); if (waitUntilTransformed) { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts index 498c105d0da49..21ebd75fa6329 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/index_endpoint_rule_alerts.ts @@ -12,6 +12,8 @@ import type { export const indexEndpointRuleAlerts = (options: { endpointAgentId: string; + endpointHostname?: string; + endpointIsolated?: boolean; count?: number; }): Cypress.Chainable< Pick & { diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.ts new file mode 100644 index 0000000000000..b00b567852026 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/isolate.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 type { ActionDetails } from '../../../../common/endpoint/types'; + +const API_ENDPOINT_ACTION_PATH = '/api/endpoint/action/*'; +export const interceptActionRequests = ( + cb: (responseBody: ActionDetails) => void, + alias: string +): void => { + cy.intercept('POST', API_ENDPOINT_ACTION_PATH, (req) => { + req.continue((res) => { + const { + body: { action, data }, + } = res; + + cb({ action, ...data }); + }); + }).as(alias); +}; + +export const sendActionResponse = (action: ActionDetails): void => { + cy.task('sendHostActionResponse', { + action, + state: { state: 'success' }, + }); +}; + +export const isolateHostWithComment = (comment: string, hostname: string): void => { + cy.getByTestSubj('isolate-host-action-item').click(); + cy.contains(`Isolate host ${hostname} from network.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(comment); +}; + +export const releaseHostWithComment = (comment: string, hostname: string): void => { + cy.contains(`${hostname} is currently isolated.`); + cy.getByTestSubj('endpointHostIsolationForm'); + cy.getByTestSubj('host_isolation_comment').type(comment); +}; + +export const openAlertDetails = (): void => { + cy.getByTestSubj('expand-event').first().click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); +}; + +export const openCaseAlertDetails = (alertId: string): void => { + cy.getByTestSubj(`comment-action-show-alert-${alertId}`).click(); + cy.getByTestSubj('take-action-dropdown-btn').click(); +}; +export const waitForReleaseOption = (alertId: string): void => { + openCaseAlertDetails(alertId); + cy.getByTestSubj('event-field-agent.status').then(($status) => { + if ($status.find('[title="Isolated"]').length > 0) { + cy.contains('Release host').click(); + } else { + cy.getByTestSubj('euiFlyoutCloseButton').click(); + openCaseAlertDetails(alertId); + cy.getByTestSubj('event-field-agent.status').within(() => { + cy.contains('Isolated'); + }); + cy.contains('Release host').click(); + } + }); +}; diff --git a/x-pack/plugins/security_solution/public/management/cypress/types.ts b/x-pack/plugins/security_solution/public/management/cypress/types.ts index 0741f7fab1ad0..97d635a3b6840 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/types.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/types.ts @@ -7,6 +7,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { ActionDetails } from '../../../common/endpoint/types'; import type { CyLoadEndpointDataOptions } from './support/plugin_handlers/endpoint_data_loader'; type PossibleChainable = @@ -41,5 +42,15 @@ export type ReturnTypeFromChainable = C extends Cyp : never; export type IndexEndpointHostsCyTaskOptions = Partial< - { count: number } & Pick + { count: number; withResponseActions: boolean } & Pick< + CyLoadEndpointDataOptions, + 'version' | 'os' | 'isolation' + > >; + +export interface HostActionResponse { + data: { + action: ActionDetails; + state: { state?: 'success' | 'failure' }; + }; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index b72d4fa30777d..40abffc508fab 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -256,15 +256,15 @@ describe('endpoint list middleware', () => { query: { agent_ids: [ '0dc3661d-6e67-46b0-af39-6f12b025fcb0', - '34634c58-24b4-4448-80f4-107fb9918494', - '5a1298e3-e607-4bc0-8ef6-6d6a811312f2', - '78c54b13-596d-4891-95f4-80092d04454b', - '445f1fd2-5f81-4ddd-bdb6-f0d1bf2efe90', - 'd77a3fc6-3096-4852-a6ee-f6b09278fbc6', - '892fcccf-1bd8-45a2-a9cc-9a7860a3cb81', - '693a3110-5ba0-4284-a264-5d78301db08c', - '554db084-64fa-4e4a-ba47-2ba713f9932b', - 'c217deb6-674d-4f97-bb1d-a3a04238e6d7', + 'fe16dda9-7f34-434c-9824-b4844880f410', + 'f412728b-929c-48d5-bdb6-5a1298e3e607', + 'd0405ddc-1e7c-48f0-93d7-d55f954bd745', + '46d78dd2-aedf-4d3f-b3a9-da445f1fd25f', + '5aafa558-26b8-4bb4-80e2-ac0644d77a3f', + 'edac2c58-1748-40c3-853c-8fab48c333d7', + '06b7223a-bb2a-428a-9021-f1c0d2267ada', + 'b8daa43b-7f73-4684-9221-dbc8b769405e', + 'fbc06310-7d41-46b8-a5ea-ceed8a993b1a', ], }, }); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx index 26e3d866ff343..8928d3aaf312b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/host_alerts_table/host_alerts_table.tsx @@ -20,6 +20,7 @@ import { } from '@elastic/eui'; import { ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { CellActionsMode } from '@kbn/cell-actions'; import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters'; import { FormattedCount } from '../../../../common/components/formatted_number'; import { HeaderSection } from '../../../../common/components/header_section'; @@ -33,6 +34,10 @@ import * as i18n from '../translations'; import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils'; import type { HostAlertsItem } from './use_host_alerts_items'; import { useHostAlertsItems } from './use_host_alerts_items'; +import { + SecurityCellActions, + SecurityCellActionsTrigger, +} from '../../../../common/components/cell_actions'; interface HostAlertsTableProps { signalIndexName: string | null; @@ -143,13 +148,27 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.ALERTS_TEXT, 'data-test-subj': 'hostSeverityAlertsTable-totalAlerts', render: (totalAlerts: number, { hostName }) => ( - handleClick({ hostName })} + - - + handleClick({ hostName })} + > + + + ), }, { @@ -157,13 +176,30 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_CRITICAL_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'critical' })} + - - + handleClick({ hostName, severity: 'critical' })} + > + + + ), }, @@ -172,9 +208,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_HIGH_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'high' })}> - - + + handleClick({ hostName, severity: 'high' })} + > + + + ), }, @@ -183,12 +239,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_MEDIUM_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'medium' })} + - - + handleClick({ hostName, severity: 'medium' })} + > + + + ), }, @@ -197,9 +270,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_LOW_LABEL, render: (count: number, { hostName }) => ( - handleClick({ hostName, severity: 'low' })}> - - + + handleClick({ hostName, severity: 'low' })} + > + + + ), }, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx index 8b0f38d0e479f..6c60bfc727b46 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/rule_alerts_table/rule_alerts_table.tsx @@ -21,6 +21,8 @@ import { import { FormattedRelative } from '@kbn/i18n-react'; import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; +import { CellActionsMode } from '@kbn/cell-actions'; +import { SecurityCellActionsTrigger } from '../../../../actions/constants'; import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters'; import { HeaderSection } from '../../../../common/components/header_section'; @@ -36,6 +38,7 @@ import { HoverVisibilityContainer } from '../../../../common/components/hover_vi import { BUTTON_CLASS as INSPECT_BUTTON_CLASS } from '../../../../common/components/inspect'; import { LastUpdatedAt } from '../../../../common/components/last_updated_at'; import { FormattedCount } from '../../../../common/components/formatted_number'; +import { SecurityCellActions } from '../../../../common/components/cell_actions'; export interface RuleAlertsTableProps { signalIndexName: string | null; @@ -95,13 +98,27 @@ export const getTableColumns: GetTableColumns = ({ name: i18n.RULE_ALERTS_COLUMN_ALERT_COUNT, 'data-test-subj': 'severityRuleAlertsTable-alertCount', render: (alertCount: number, { name }) => ( - openRuleInAlertsPage(name)} + - - + openRuleInAlertsPage(name)} + > + + + ), }, { diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx index ddaf75cba0f8a..914c3c93ff240 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/user_alerts_table/user_alerts_table.tsx @@ -20,6 +20,8 @@ import { } from '@elastic/eui'; import { ALERT_SEVERITY } from '@kbn/rule-data-utils'; +import { CellActionsMode } from '@kbn/cell-actions'; +import { SecurityCellActionsTrigger } from '../../../../actions/constants'; import { useNavigateToAlertsPageWithFilters } from '../../../../common/hooks/use_navigate_to_alerts_page_with_filters'; import { FormattedCount } from '../../../../common/components/formatted_number'; import { HeaderSection } from '../../../../common/components/header_section'; @@ -32,6 +34,7 @@ import * as i18n from '../translations'; import { ITEMS_PER_PAGE, SEVERITY_COLOR } from '../utils'; import type { UserAlertsItem } from './use_user_alerts_items'; import { useUserAlertsItems } from './use_user_alerts_items'; +import { SecurityCellActions } from '../../../../common/components/cell_actions'; interface UserAlertsTableProps { signalIndexName: string | null; @@ -142,13 +145,27 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.ALERTS_TEXT, 'data-test-subj': 'userSeverityAlertsTable-totalAlerts', render: (totalAlerts: number, { userName }) => ( - handleClick({ userName })} + - - + handleClick({ userName })} + > + + + ), }, { @@ -156,13 +173,30 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_CRITICAL_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'critical' })} + - - + handleClick({ userName, severity: 'critical' })} + > + + + ), }, @@ -171,9 +205,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_HIGH_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'high' })}> - - + + handleClick({ userName, severity: 'high' })} + > + + + ), }, @@ -182,12 +236,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_MEDIUM_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'medium' })} + - - + handleClick({ userName, severity: 'medium' })} + > + + + ), }, @@ -196,9 +267,29 @@ const getTableColumns: GetTableColumns = (handleClick) => [ name: i18n.STATUS_LOW_LABEL, render: (count: number, { userName }) => ( - handleClick({ userName, severity: 'low' })}> - - + + handleClick({ userName, severity: 'low' })} + > + + + ), }, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.tsx index 4f74100ff4888..25c97c1ababe8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/anomalies/index.tsx @@ -60,7 +60,7 @@ export const EntityAnalyticsAnomalies = () => { const [updatedAt, setUpdatedAt] = useState(Date.now()); const { toggleStatus, setToggleStatus } = useQueryToggle(TABLE_QUERY_ID); - const { deleteQuery, setQuery, from, to } = useGlobalTime(false); + const { deleteQuery, setQuery, from, to } = useGlobalTime(); const { isLoading: isSearchLoading, data, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx index 28e5c696d1f52..85cc8c0043897 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/header/index.tsx @@ -41,7 +41,7 @@ const HOST_RISK_QUERY_ID = 'hostRiskScoreKpiQuery'; const USER_RISK_QUERY_ID = 'userRiskScoreKpiQuery'; export const EntityAnalyticsHeader = () => { - const { from, to } = useGlobalTime(false); + const { from, to } = useGlobalTime(); const timerange = useMemo( () => ({ from, diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx index eee34e79d99c6..27b069c45de75 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/columns.tsx @@ -140,17 +140,31 @@ export const getRiskScoreColumns = ( truncateText: false, mobileOptions: { show: true }, render: (alertCount: number, risk) => ( - - openEntityOnAlertsPage( - riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name - ) - } + - - + + openEntityOnAlertsPage( + riskEntity === RiskScoreEntity.host ? risk.host.name : risk.user.name + ) + } + > + + + ), }, ]; diff --git a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx index 80a750a8bce14..7869a234ccd50 100644 --- a/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/entity_analytics/risk_score/index.test.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import { render, fireEvent } from '@testing-library/react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import { TestProviders } from '../../../../common/mock'; import { EntityAnalyticsRiskScores } from '.'; -import type { UserRiskScore } from '../../../../../common/search_strategy'; import { RiskScoreEntity, RiskSeverity } from '../../../../../common/search_strategy'; import type { SeverityCount } from '../../../../explore/components/risk_score/severity/types'; import { useRiskScore, useRiskScoreKpi } from '../../../../explore/containers/risk_score'; @@ -146,17 +145,17 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( expect(queryByTestId('entity_analytics_content')).not.toBeInTheDocument(); }); - it('renders alerts count', () => { + it('renders alerts count', async () => { mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); mockUseRiskScoreKpi.mockReturnValue({ severityCount: mockSeverityCount, loading: false, }); const alertsCount = 999; - const data: UserRiskScore[] = [ + const data = [ { '@timestamp': '1234567899', - user: { + [riskEntity]: { name: 'testUsermame', risk: { rule_risks: [], @@ -176,10 +175,12 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( ); - expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString()); + await waitFor(() => { + expect(queryByTestId('risk-score-alerts')).toHaveTextContent(alertsCount.toString()); + }); }); - it('navigates to alerts page with filters when alerts count is clicked', () => { + it('navigates to alerts page with filters when alerts count is clicked', async () => { mockUseQueryToggle.mockReturnValue({ toggleStatus: true, setToggleStatus: jest.fn() }); mockUseRiskScoreKpi.mockReturnValue({ severityCount: mockSeverityCount, @@ -211,13 +212,15 @@ describe.each([RiskScoreEntity.host, RiskScoreEntity.user])( fireEvent.click(getByTestId('risk-score-alerts')); - expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([ - { - title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User', - fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name', - selectedOptions: [name], - }, - ]); + await waitFor(() => { + expect(mockOpenAlertsPageWithFilters.mock.calls[0][0]).toEqual([ + { + title: riskEntity === RiskScoreEntity.host ? 'Host' : 'User', + fieldName: riskEntity === RiskScoreEntity.host ? 'host.name' : 'user.name', + selectedOptions: [name], + }, + ]); + }); }); } ); diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 6238392e888be..fdbee5da33e9f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -317,7 +317,7 @@ export const MoreContainer = React.memo( PolicyConfig): Packa describe('Policy-Changing license watcher', () => { const logger = loggingSystemMock.create().get('license_watch.test'); - const cloudServiceMock = cloudMock.createSetup(); const soStartMock = savedObjectsServiceMock.createStartContract(); const esStartMock = elasticsearchServiceMock.createStart(); let packagePolicySvcMock: jest.Mocked; @@ -53,13 +51,7 @@ describe('Policy-Changing license watcher', () => { // mock a license-changing service to test reactivity const licenseEmitter: Subject = new Subject(); const licenseService = new LicenseService(); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // swap out watch function, just to ensure it gets called when a license change happens const mockWatch = jest.fn(); @@ -104,13 +96,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); await pw.watch(Gold); // just manually trigger with a given license expect(packagePolicySvcMock.list.mock.calls.length).toBe(3); // should have asked for 3 pages of resuts @@ -137,13 +123,7 @@ describe('Policy-Changing license watcher', () => { perPage: 100, }); - const pw = new PolicyWatcher( - packagePolicySvcMock, - soStartMock, - esStartMock, - cloudServiceMock, - logger - ); + const pw = new PolicyWatcher(packagePolicySvcMock, soStartMock, esStartMock, logger); // emulate a license change below paid tier await pw.watch(Basic); diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts index dded85b559c6d..195c8509e60d5 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.ts @@ -17,7 +17,6 @@ import type { } from '@kbn/core/server'; import type { PackagePolicy } from '@kbn/fleet-plugin/common'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '@kbn/fleet-plugin/common'; -import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; import type { ILicense } from '@kbn/licensing-plugin/common/types'; import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server'; @@ -35,19 +34,16 @@ export class PolicyWatcher { private policyService: PackagePolicyClient; private subscription: Subscription | undefined; private soStart: SavedObjectsServiceStart; - private cloud: CloudSetup; constructor( policyService: PackagePolicyClient, soStart: SavedObjectsServiceStart, esStart: ElasticsearchServiceStart, - cloud: CloudSetup, logger: Logger ) { this.policyService = policyService; this.esClient = esStart.client.asInternalUser; this.logger = logger; this.soStart = soStart; - this.cloud = cloud; } /** @@ -105,9 +101,6 @@ export class PolicyWatcher { for (const policy of response.items as PolicyData[]) { const updatePolicy = getPolicyDataForUpdate(policy); const policyConfig = updatePolicy.inputs[0].config.policy.value; - updatePolicy.inputs[0].config.policy.value.meta.license = license.type || ''; - // add cloud info to policy meta - updatePolicy.inputs[0].config.policy.value.meta.cloud = this.cloud?.isCloudEnabled; try { if (!isEndpointPolicyValidForLicense(policyConfig, license)) { diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index 30c7e776e718e..c415fc287fbec 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -366,7 +366,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -382,7 +383,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -412,7 +414,8 @@ describe('ingest_integration tests ', () => { logger, licenseService, endpointAppContextMock.featureUsageService, - endpointAppContextMock.endpointMetadataService + endpointAppContextMock.endpointMetadataService, + cloudService ); const policyConfig = generator.generatePolicyPackagePolicy(); policyConfig.inputs[0]!.config!.policy.value = mockPolicy; @@ -427,6 +430,66 @@ describe('ingest_integration tests ', () => { }); }); + describe('package policy update callback when meta fields should be updated', () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + + beforeEach(() => { + licenseEmitter.next(Platinum); // set license level to platinum + }); + it('updates successfully when meta fields differ from services', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = false; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'gold'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + + it('meta fields stay the same where there is no difference', async () => { + const mockPolicy = policyFactory(); + mockPolicy.meta.cloud = true; // cloud mock will return true + mockPolicy.meta.license = 'platinum'; // license is set to emit platinum + const logger = loggingSystemMock.create().get('ingest_integration.test'); + const callback = getPackagePolicyUpdateCallback( + logger, + licenseService, + endpointAppContextMock.featureUsageService, + endpointAppContextMock.endpointMetadataService, + cloudService + ); + const policyConfig = generator.generatePolicyPackagePolicy(); + // values should be updated + policyConfig.inputs[0]!.config!.policy.value.meta.cloud = true; + policyConfig.inputs[0]!.config!.policy.value.meta.license = 'platinum'; + const updatedPolicyConfig = await callback( + policyConfig, + soClient, + esClient, + requestContextMock.convertContext(ctx), + req + ); + expect(updatedPolicyConfig.inputs[0]!.config!.policy.value).toEqual(mockPolicy); + }); + }); + describe('package policy delete callback', () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index fd6f85af1a045..7e68c63f07593 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -44,6 +44,17 @@ const isEndpointPackagePolicy = ( return packagePolicy.package?.name === 'endpoint'; }; +const shouldUpdateMetaValues = ( + endpointPackagePolicy: PolicyConfig, + currentLicenseType: string, + currentCloudInfo: boolean +) => { + return ( + endpointPackagePolicy.meta.license !== currentLicenseType || + endpointPackagePolicy.meta.cloud !== currentCloudInfo + ); +}; + /** * Callback to handle creation of PackagePolicies in Fleet */ @@ -152,7 +163,8 @@ export const getPackagePolicyUpdateCallback = ( logger: Logger, licenseService: LicenseService, featureUsageService: FeatureUsageService, - endpointMetadataService: EndpointMetadataService + endpointMetadataService: EndpointMetadataService, + cloud: CloudSetup ): PutPackagePolicyUpdateCallback => { return async (newPackagePolicy: NewPackagePolicy): Promise => { if (!isEndpointPackagePolicy(newPackagePolicy)) { @@ -170,6 +182,22 @@ export const getPackagePolicyUpdateCallback = ( notifyProtectionFeatureUsage(newPackagePolicy, featureUsageService, endpointMetadataService); + const newEndpointPackagePolicy = newPackagePolicy.inputs[0].config?.policy + ?.value as PolicyConfig; + + if ( + newPackagePolicy.inputs[0].config?.policy?.value && + shouldUpdateMetaValues( + newEndpointPackagePolicy, + licenseService.getLicenseType(), + cloud?.isCloudEnabled + ) + ) { + newEndpointPackagePolicy.meta.license = licenseService.getLicenseType(); + newEndpointPackagePolicy.meta.cloud = cloud?.isCloudEnabled; + newPackagePolicy.inputs[0].config.policy.value = newEndpointPackagePolicy; + } + return newPackagePolicy; }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts index 7df109deb6f3b..848a0933da542 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/utils.ts @@ -65,7 +65,7 @@ export const getOutputRuleAlertForRest = (): RuleResponse => ({ severity_mapping: [], updated_by: 'elastic', tags: [], - throttle: 'no_actions', + throttle: undefined, threat: getThreatMock(), exceptions_list: getListArrayMock(), filters: [ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts index 27b9111b0434d..974ea9cc7ecc5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/api/rules/bulk_actions/route.ts @@ -432,8 +432,10 @@ export const performBulkActionRoute = ( } let shouldDuplicateExceptions = true; + let shouldDuplicateExpiredExceptions = true; if (body.duplicate !== undefined) { shouldDuplicateExceptions = body.duplicate.include_exceptions; + shouldDuplicateExpiredExceptions = body.duplicate.include_expired_exceptions; } const duplicateRuleToCreate = await duplicateRule({ @@ -449,6 +451,7 @@ export const performBulkActionRoute = ( ? await duplicateExceptions({ ruleId: rule.params.ruleId, exceptionLists: rule.params.exceptionsList, + includeExpiredExceptions: shouldDuplicateExpiredExceptions, exceptionsClient, }) : []; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.test.ts new file mode 100644 index 0000000000000..aeb593ec33fff --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { duplicateExceptions } from './duplicate_exceptions'; +import { getExceptionListClientMock } from '@kbn/lists-plugin/server/services/exception_lists/exception_list_client.mock'; +import type { List } from '@kbn/securitysolution-io-ts-list-types'; +import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import type { ExceptionListClient } from '@kbn/lists-plugin/server'; +import { getDetectionsExceptionListSchemaMock } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; + +jest.mock('uuid', () => ({ + v4: jest.fn(), +})); + +describe('duplicateExceptions', () => { + let exceptionsClient: ExceptionListClient; + + beforeAll(() => { + exceptionsClient = getExceptionListClientMock(); + exceptionsClient.duplicateExceptionListAndItems = jest.fn(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + it('returns empty array if no exceptions to duplicate', async () => { + const result = await duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [], + exceptionsClient, + includeExpiredExceptions: false, + }); + + expect(result).toEqual([]); + }); + + it('returns array referencing the same shared exception lists if no rule default exceptions included', async () => { + const sharedExceptionListReference: List = { + type: ExceptionListTypeEnum.DETECTION, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + const result = await duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [sharedExceptionListReference], + exceptionsClient, + includeExpiredExceptions: false, + }); + + expect(exceptionsClient.duplicateExceptionListAndItems).not.toHaveBeenCalled(); + expect(result).toEqual([sharedExceptionListReference]); + }); + + it('duplicates rule default and shared exceptions', async () => { + const newDefaultRuleList = { + ...getDetectionsExceptionListSchemaMock(), + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'rule_default_list_dupe', + namespace_type: 'single', + id: '123-abc', + }; + + exceptionsClient.getExceptionList = jest.fn().mockResolvedValue({ + ...getDetectionsExceptionListSchemaMock(), + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'rule_default_list', + namespace_type: 'single', + id: '5678', + }); + exceptionsClient.duplicateExceptionListAndItems = jest + .fn() + .mockResolvedValue(newDefaultRuleList); + + const sharedExceptionListReference: List = { + type: ExceptionListTypeEnum.DETECTION, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + const ruleDefaultListReference: List = { + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'rule_default_list', + namespace_type: 'single', + id: '5678', + }; + + const result = await duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [sharedExceptionListReference, ruleDefaultListReference], + exceptionsClient, + includeExpiredExceptions: false, + }); + + expect(result).toEqual([ + sharedExceptionListReference, + { + type: newDefaultRuleList.type, + namespace_type: newDefaultRuleList.namespace_type, + id: newDefaultRuleList.id, + list_id: newDefaultRuleList.list_id, + }, + ]); + }); + + it('throws error if rule default list to duplicate not found', async () => { + exceptionsClient.getExceptionList = jest.fn().mockResolvedValue(null); + + const ruleDefaultListReference: List = { + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + await expect(() => + duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [ruleDefaultListReference], + exceptionsClient, + includeExpiredExceptions: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unable to duplicate rule default exceptions - unable to find their container with list_id: "my_list"]` + ); + }); + + it('throws error if list duplication returns null', async () => { + exceptionsClient.getExceptionList = jest.fn().mockResolvedValue({ + ...getDetectionsExceptionListSchemaMock(), + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }); + exceptionsClient.duplicateExceptionListAndItems = jest.fn().mockResolvedValue(null); + + const ruleDefaultListReference: List = { + type: ExceptionListTypeEnum.RULE_DEFAULT, + list_id: 'my_list', + namespace_type: 'single', + id: '1234', + }; + + await expect(() => + duplicateExceptions({ + ruleId: 'rule_123', + exceptionLists: [ruleDefaultListReference], + exceptionsClient, + includeExpiredExceptions: false, + }) + ).rejects.toMatchInlineSnapshot( + `[Error: Unable to duplicate rule default exception items for rule_id: rule_123]` + ); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts index 496a91ba55963..82f13adc4534c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_exceptions.ts @@ -5,23 +5,42 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; + import type { ExceptionListClient } from '@kbn/lists-plugin/server'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import type { RuleParams } from '../../../rule_schema'; +const ERROR_DUPLICATING = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.cloneExceptions.errorDuplicatingList', + { + defaultMessage: + 'Unable to duplicate rule default exceptions - unable to find their container with list_id:', + } +); + +const ERROR_DUPLICATING_ITEMS = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.cloneExceptions.errorDuplicatingListItems', + { + defaultMessage: 'Unable to duplicate rule default exception items for rule_id:', + } +); + interface DuplicateExceptionsParams { ruleId: RuleParams['ruleId']; exceptionLists: RuleParams['exceptionsList']; exceptionsClient: ExceptionListClient | undefined; + includeExpiredExceptions: boolean; } export const duplicateExceptions = async ({ ruleId, exceptionLists, exceptionsClient, + includeExpiredExceptions, }: DuplicateExceptionsParams): Promise => { - if (exceptionLists == null) { + if (exceptionLists == null || !exceptionLists.length) { return []; } @@ -37,24 +56,36 @@ export const duplicateExceptions = async ({ // For rule_default list (exceptions that live only on a single rule), we need // to create a new rule_default list to assign to duplicated rule if (ruleDefaultList != null && exceptionsClient != null) { - const ruleDefaultExceptionList = await exceptionsClient.duplicateExceptionListAndItems({ + // fetch list container + const listToDuplicate = await exceptionsClient.getExceptionList({ + id: undefined, listId: ruleDefaultList.list_id, namespaceType: ruleDefaultList.namespace_type, }); - if (ruleDefaultExceptionList == null) { - throw new Error(`Unable to duplicate rule default exception items for rule_id: ${ruleId}`); - } + if (listToDuplicate == null) { + throw new Error(`${ERROR_DUPLICATING} "${ruleDefaultList.list_id}"`); + } else { + const ruleDefaultExceptionList = await exceptionsClient.duplicateExceptionListAndItems({ + list: listToDuplicate, + namespaceType: ruleDefaultList.namespace_type, + includeExpiredExceptions, + }); - return [ - ...sharedLists, - { - id: ruleDefaultExceptionList.id, - list_id: ruleDefaultExceptionList.list_id, - namespace_type: ruleDefaultExceptionList.namespace_type, - type: ruleDefaultExceptionList.type, - }, - ]; + if (ruleDefaultExceptionList == null) { + throw new Error(`${ERROR_DUPLICATING_ITEMS} ${ruleId}`); + } + + return [ + ...sharedLists, + { + id: ruleDefaultExceptionList.id, + list_id: ruleDefaultExceptionList.list_id, + namespace_type: ruleDefaultExceptionList.namespace_type, + type: ruleDefaultExceptionList.type, + }, + ]; + } } // If no rule_default list exists, we can just return diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts index b441d071c21c1..08a53c007dc06 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.test.ts @@ -109,8 +109,6 @@ describe('duplicateRule', () => { consumer: rule.consumer, schedule: rule.schedule, actions: rule.actions, - throttle: null, // TODO: fix? - notifyWhen: null, // TODO: fix? enabled: false, // covered in a separate test }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts index 4a99085123b41..315517504def4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/actions/duplicate_rule.ts @@ -11,6 +11,7 @@ import { ruleTypeMappings } from '@kbn/securitysolution-rules'; import type { SanitizedRule } from '@kbn/alerting-plugin/common'; import { SERVER_APP_ID } from '../../../../../../common/constants'; import type { InternalRuleCreate, RuleParams } from '../../../rule_schema'; +import { transformToActionFrequency } from '../../normalization/rule_actions'; const DUPLICATE_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.cloneRule.duplicateTitle', @@ -33,6 +34,7 @@ export const duplicateRule = async ({ rule }: DuplicateRuleParams): Promise - ({ - field: 'throttle', - operation: 'set', - value: transformToAlertThrottle(throttle), - } as const); - -const getNotifyWhenOperation = (throttle: string) => - ({ - field: 'notifyWhen', - operation: 'set', - value: transformToNotifyWhen(throttle), - } as const); +import { transformToActionFrequency } from '../../normalization/rule_actions'; /** * converts bulk edit action to format of rulesClient.bulkEdit operation @@ -70,10 +55,8 @@ export const bulkEditActionToRulesClientOperation = ( { field: 'actions', operation: 'add', - value: action.value.actions, + value: transformToActionFrequency(action.value.actions, action.value.throttle), }, - getThrottleOperation(action.value.throttle), - getNotifyWhenOperation(action.value.throttle), ]; case BulkActionEditType.set_rule_actions: @@ -81,10 +64,8 @@ export const bulkEditActionToRulesClientOperation = ( { field: 'actions', operation: 'set', - value: action.value.actions, + value: transformToActionFrequency(action.value.actions, action.value.throttle), }, - getThrottleOperation(action.value.throttle), - getNotifyWhenOperation(action.value.throttle), ]; // schedule actions diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts index cfb051273255a..6e4d573a880e0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/patch_rules.test.ts @@ -123,6 +123,7 @@ describe('patchRules', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', }, group: 'default', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }), @@ -158,6 +159,7 @@ describe('patchRules', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} signals', }, group: 'default', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts index 1996cb1652ab6..070a81692f3d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/crud/update_rules.ts @@ -12,7 +12,7 @@ import type { RuleUpdateProps } from '../../../../../../common/detection_engine/ import { transformRuleToAlertAction } from '../../../../../../common/detection_engine/transform_actions'; import type { InternalRuleUpdate, RuleParams, RuleAlertType } from '../../../rule_schema'; -import { transformToAlertThrottle, transformToNotifyWhen } from '../../normalization/rule_actions'; +import { transformToActionFrequency } from '../../normalization/rule_actions'; import { typeSpecificSnakeToCamel } from '../../normalization/rule_converters'; export interface UpdateRulesOptions { @@ -30,6 +30,9 @@ export const updateRules = async ({ return null; } + const alertActions = ruleUpdate.actions?.map(transformRuleToAlertAction) ?? []; + const actions = transformToActionFrequency(alertActions, ruleUpdate.throttle); + const typeSpecificParams = typeSpecificSnakeToCamel(ruleUpdate); const enabled = ruleUpdate.enabled ?? true; const newInternalRule: InternalRuleUpdate = { @@ -70,9 +73,7 @@ export const updateRules = async ({ ...typeSpecificParams, }, schedule: { interval: ruleUpdate.interval ?? '5m' }, - actions: ruleUpdate.actions != null ? ruleUpdate.actions.map(transformRuleToAlertAction) : [], - throttle: transformToAlertThrottle(ruleUpdate.throttle), - notifyWhen: transformToNotifyWhen(ruleUpdate.throttle), + actions, }; const update = await rulesClient.update({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts index 038dfbc9152d6..c86387f26f50a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_all.test.ts @@ -131,7 +131,6 @@ describe('getExportAll', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), @@ -275,6 +274,7 @@ describe('getExportAll', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.slack', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], building_block_type: 'default', @@ -313,7 +313,6 @@ describe('getExportAll', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'rule', note: '# Investigative notes', version: 1, revision: 0, @@ -416,6 +415,7 @@ describe('getExportAll', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.email', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts index de76ef3b23d8b..35151f1b4b1d6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/logic/export/get_export_by_object_ids.test.ts @@ -128,7 +128,6 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', note: '# Investigative notes', version: 1, exceptions_list: getListArrayMock(), @@ -284,6 +283,7 @@ describe('get_export_by_object_ids', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.slack', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], building_block_type: 'default', @@ -322,7 +322,6 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'rule', note: '# Investigative notes', version: 1, revision: 0, @@ -426,6 +425,7 @@ describe('get_export_by_object_ids', () => { message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: '.email', + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }) @@ -508,7 +508,7 @@ describe('get_export_by_object_ids', () => { to: 'now', type: 'query', threat: getThreatMock(), - throttle: 'no_actions', + throttle: undefined, note: '# Investigative notes', version: 1, revision: 0, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts index d0d5edad970f0..33fcf0bd29c20 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.test.ts @@ -5,7 +5,9 @@ * 2.0. */ +import type { SanitizedRuleAction } from '@kbn/alerting-plugin/common'; import { + NOTIFICATION_DEFAULT_FREQUENCY, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE, } from '../../../../../common/constants'; @@ -14,6 +16,7 @@ import type { RuleAlertType } from '../../rule_schema'; import { transformFromAlertThrottle, + transformToActionFrequency, transformToAlertThrottle, transformToNotifyWhen, } from './rule_actions'; @@ -151,4 +154,152 @@ describe('Rule actions normalization', () => { ).toEqual(NOTIFICATION_THROTTLE_RULE); }); }); + + describe('transformToActionFrequency', () => { + describe('actions without frequencies', () => { + const actionsWithoutFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + }, + ]; + + test.each([undefined, null, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE])( + `it sets each action's frequency attribute to default value when 'throttle' is '%s'`, + (throttle) => { + expect(transformToActionFrequency(actionsWithoutFrequencies, throttle)).toEqual( + actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })) + ); + } + ); + + test.each(['47s', '10m', '3h', '101d'])( + `it correctly transforms 'throttle = %s' and sets it as a frequency of each action`, + (throttle) => { + expect(transformToActionFrequency(actionsWithoutFrequencies, throttle)).toEqual( + actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })) + ); + } + ); + }); + + describe('actions with frequencies', () => { + const actionsWithFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + frequency: { + summary: false, + throttle: '1s', + notifyWhen: 'onThrottleInterval', + }, + }, + ]; + + test.each([ + undefined, + null, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '1h', + '1d', + ])(`it does not change actions frequency attributes when 'throttle' is '%s'`, (throttle) => { + expect(transformToActionFrequency(actionsWithFrequencies, throttle)).toEqual( + actionsWithFrequencies + ); + }); + }); + + describe('some actions with frequencies', () => { + const someActionsWithFrequencies: SanitizedRuleAction[] = [ + { + group: 'group', + id: 'id-123', + actionTypeId: 'id-456', + params: {}, + frequency: { + summary: true, + throttle: null, + notifyWhen: 'onActiveAlert', + }, + }, + { + group: 'group', + id: 'id-789', + actionTypeId: 'id-012', + params: {}, + frequency: { + summary: false, + throttle: '1s', + notifyWhen: 'onThrottleInterval', + }, + }, + { + group: 'group', + id: 'id-345', + actionTypeId: 'id-678', + params: {}, + }, + ]; + + test.each([undefined, null, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE])( + `it overrides each action's frequency attribute to default value when 'throttle' is '%s'`, + (throttle) => { + expect(transformToActionFrequency(someActionsWithFrequencies, throttle)).toEqual( + someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })) + ); + } + ); + + test.each(['47s', '10m', '3h', '101d'])( + `it correctly transforms 'throttle = %s' and overrides frequency attribute of each action`, + (throttle) => { + expect(transformToActionFrequency(someActionsWithFrequencies, throttle)).toEqual( + someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })) + ); + } + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts index 51dbe9d80f986..d0e909cc5f329 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { RuleNotifyWhenType } from '@kbn/alerting-plugin/common'; +import type { RuleActionFrequency, RuleNotifyWhenType } from '@kbn/alerting-plugin/common'; import { NOTIFICATION_THROTTLE_NO_ACTIONS, @@ -14,6 +14,38 @@ import { import type { RuleAlertType } from '../../rule_schema'; +export const transformToFrequency = (throttle: string | null | undefined): RuleActionFrequency => { + return { + summary: true, + notifyWhen: transformToNotifyWhen(throttle) ?? 'onActiveAlert', + throttle: transformToAlertThrottle(throttle), + }; +}; + +interface ActionWithFrequency { + frequency?: RuleActionFrequency; +} + +/** + * The action level `frequency` attribute should always take precedence over the rule level `throttle` + * Frequency's default value is `{ summary: true, throttle: null, notifyWhen: 'onActiveAlert' }` + * + * The transformation follows the next rules: + * - Both rule level `throttle` and all actions have `frequency` are set: we will ignore rule level `throttle` + * - Rule level `throttle` set and actions don't have `frequency` set: we will transform rule level `throttle` in action level `frequency` + * - All actions have `frequency` set: do nothing + * - Neither of them is set: we will set action level `frequency` to default value + * - Rule level `throttle` and some of the actions have `frequency` set: we will transform rule level `throttle` and set it to actions without the frequency attribute + * - Only some actions have `frequency` set and there is no rule level `throttle`: we will set default `frequency` to actions without frequency attribute + */ +export const transformToActionFrequency = ( + actions: T[], + throttle: string | null | undefined +): T[] => { + const defaultFrequency = transformToFrequency(throttle); + return actions.map((action) => ({ ...action, frequency: action.frequency ?? defaultFrequency })); +}; + /** * Given a throttle from a "security_solution" rule this will transform it into an "alerting" notifyWhen * on their saved object. diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts index 7296e90bf73e7..66d873c2c0935 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/normalization/rule_converters.ts @@ -39,10 +39,10 @@ import { } from '../../../../../common/detection_engine/rule_schema'; import { + transformAlertToRuleAction, transformAlertToRuleResponseAction, transformRuleToAlertAction, transformRuleToAlertResponseAction, - transformAlertToRuleAction, } from '../../../../../common/detection_engine/transform_actions'; import { @@ -73,11 +73,7 @@ import type { NewTermsRuleParams, NewTermsSpecificRuleParams, } from '../../rule_schema'; -import { - transformFromAlertThrottle, - transformToAlertThrottle, - transformToNotifyWhen, -} from './rule_actions'; +import { transformFromAlertThrottle, transformToActionFrequency } from './rule_actions'; import { convertAlertSuppressionToCamel, convertAlertSuppressionToSnake } from '../utils/utils'; import { createRuleExecutionSummary } from '../../rule_monitoring'; @@ -399,6 +395,11 @@ export const convertPatchAPIToInternalSchema = ( ): InternalRuleUpdate => { const typeSpecificParams = patchTypeSpecificSnakeToCamel(nextParams, existingRule.params); const existingParams = existingRule.params; + + const alertActions = nextParams.actions?.map(transformRuleToAlertAction) ?? existingRule.actions; + const throttle = nextParams.throttle ?? transformFromAlertThrottle(existingRule); + const actions = transformToActionFrequency(alertActions, throttle); + return { name: nextParams.name ?? existingRule.name, tags: nextParams.tags ?? existingRule.tags, @@ -438,15 +439,7 @@ export const convertPatchAPIToInternalSchema = ( ...typeSpecificParams, }, schedule: { interval: nextParams.interval ?? existingRule.schedule.interval }, - actions: nextParams.actions - ? nextParams.actions.map(transformRuleToAlertAction) - : existingRule.actions, - throttle: nextParams.throttle - ? transformToAlertThrottle(nextParams.throttle) - : existingRule.throttle ?? null, - notifyWhen: nextParams.throttle - ? transformToNotifyWhen(nextParams.throttle) - : existingRule.notifyWhen ?? null, + actions, }; }; @@ -462,6 +455,10 @@ export const convertCreateAPIToInternalSchema = ( ): InternalRuleCreate => { const typeSpecificParams = typeSpecificSnakeToCamel(input); const newRuleId = input.rule_id ?? uuidv4(); + + const alertActions = input.actions?.map(transformRuleToAlertAction) ?? []; + const actions = transformToActionFrequency(alertActions, input.throttle); + return { name: input.name, tags: input.tags ?? [], @@ -502,9 +499,7 @@ export const convertCreateAPIToInternalSchema = ( }, schedule: { interval: input.interval ?? '5m' }, enabled: input.enabled ?? defaultEnabled, - actions: input.actions?.map(transformRuleToAlertAction) ?? [], - throttle: transformToAlertThrottle(input.throttle), - notifyWhen: transformToNotifyWhen(input.throttle), + actions, }; }; @@ -651,6 +646,10 @@ export const internalRuleToAPIResponse = ( const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => (obj as ResolvedSanitizedRule).outcome != null; + const alertActions = rule.actions.map(transformAlertToRuleAction); + const throttle = transformFromAlertThrottle(rule); + const actions = transformToActionFrequency(alertActions, throttle); + return { // saved object properties outcome: isResolvedRule(rule) ? rule.outcome : undefined, @@ -672,8 +671,8 @@ export const internalRuleToAPIResponse = ( // Type specific security solution rule params ...typeSpecificCamelToSnake(rule.params), // Actions - throttle: transformFromAlertThrottle(rule), - actions: rule.actions.map(transformAlertToRuleAction), + throttle: undefined, + actions, // Execution summary execution_summary: executionSummary ?? undefined, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts index bae75363ff49a..373842fe31860 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_management/utils/validate.test.ts @@ -43,7 +43,7 @@ export const ruleOutput = (): RuleResponse => ({ tags: [], to: 'now', type: 'query', - throttle: 'no_actions', + throttle: undefined, threat: getThreatMock(), version: 1, revision: 0, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts index c0cb0cefa234e..c68eab1baec36 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/__mocks__/index.ts @@ -40,6 +40,7 @@ const ruleExecutionLogForExecutorsMock = { ruleId: context.ruleId ?? 'some rule id', ruleUuid: context.ruleUuid ?? 'some rule uuid', ruleName: context.ruleName ?? 'Some rule', + ruleRevision: context.ruleRevision ?? 0, ruleType: context.ruleType ?? 'some rule type', spaceId: context.spaceId ?? 'some space id', }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts index a0cbe754d6b3c..a8d61aa7dda84 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client.ts @@ -50,7 +50,7 @@ export const createClientForExecutors = ( const baseLogSuffix = baseCorrelationIds.getLogSuffix(); const baseLogMeta = baseCorrelationIds.getLogMeta(); - const { executionId, ruleId, ruleUuid, ruleName, ruleType, spaceId } = context; + const { executionId, ruleId, ruleUuid, ruleName, ruleRevision, ruleType, spaceId } = context; const client: IRuleExecutionLogForExecutors = { get context() { @@ -140,6 +140,7 @@ export const createClientForExecutors = ( ruleId, ruleUuid, ruleName, + ruleRevision, ruleType, spaceId, executionId, @@ -202,6 +203,7 @@ export const createClientForExecutors = ( ruleId, ruleUuid, ruleName, + ruleRevision, ruleType, spaceId, executionId, @@ -213,6 +215,7 @@ export const createClientForExecutors = ( ruleId, ruleUuid, ruleName, + ruleRevision, ruleType, spaceId, executionId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts index cb65b42d50b69..54cad1f72be07 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/client_for_executors/client_interface.ts @@ -92,6 +92,11 @@ export interface RuleExecutionContext { */ ruleName: string; + /** + * Current revision of the rule being execution (rule.revision) + */ + ruleRevision: number; + /** * Alerting Framework's rule type id of the rule being executed. */ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts index aa1fcf36aba68..ceed2f1d3a739 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_monitoring/logic/rule_execution_log/event_log/event_log_writer.ts @@ -31,6 +31,7 @@ export interface BaseArgs { ruleId: string; ruleUuid: string; ruleName: string; + ruleRevision: number; ruleType: string; spaceId: string; executionId: string; @@ -83,6 +84,7 @@ export const createEventLogWriter = (eventLogService: IEventLogService): IEventL execution: { uuid: args.executionId, }, + revision: args.ruleRevision, }, }, space_ids: [args.spaceId], @@ -126,6 +128,7 @@ export const createEventLogWriter = (eventLogService: IEventLogService): IEventL status: args.newStatus, status_order: ruleExecutionStatusToNumber(args.newStatus), }, + revision: args.ruleRevision, }, }, space_ids: [args.spaceId], @@ -167,6 +170,7 @@ export const createEventLogWriter = (eventLogService: IEventLogService): IEventL uuid: args.executionId, metrics: args.metrics, }, + revision: args.ruleRevision, }, }, space_ids: [args.spaceId], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts index 10a213a5d8b2f..44fa44ee01892 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.mock.ts @@ -213,10 +213,10 @@ export const getRuleConfigMock = (type: string = 'rule-type'): SanitizedRuleConf consumer: 'sample consumer', notifyWhen: null, producer: 'sample producer', + revision: 0, ruleTypeId: `${type}-id`, ruleTypeName: type, muteAll: false, - revision: 0, snoozeSchedule: [], }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts index e204aeb7bc50f..0fbddc2a2236c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema/model/rule_schemas.ts @@ -14,6 +14,7 @@ import { RiskScore, RiskScoreMapping, RuleActionArrayCamel, + RuleActionNotifyWhen, RuleActionThrottle, RuleIntervalFrom, RuleIntervalTo, @@ -259,13 +260,6 @@ export interface CompleteRule { ruleConfig: SanitizedRuleConfig; } -export const notifyWhen = t.union([ - t.literal('onActionGroupChange'), - t.literal('onActiveAlert'), - t.literal('onThrottleInterval'), - t.null, -]); - export const allRuleTypes = t.union([ t.literal(SIGNALS_ID), t.literal(EQL_RULE_TYPE_ID), @@ -291,7 +285,7 @@ const internalRuleCreateRequired = t.type({ }); const internalRuleCreateOptional = t.partial({ throttle: t.union([RuleActionThrottle, t.null]), - notifyWhen, + notifyWhen: t.union([RuleActionNotifyWhen, t.null]), }); export const internalRuleCreate = t.intersection([ internalRuleCreateOptional, @@ -310,7 +304,7 @@ const internalRuleUpdateRequired = t.type({ }); const internalRuleUpdateOptional = t.partial({ throttle: t.union([RuleActionThrottle, t.null]), - notifyWhen, + notifyWhen: t.union([RuleActionNotifyWhen, t.null]), }); export const internalRuleUpdate = t.intersection([ internalRuleUpdateOptional, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index a22a95adf307b..e2650f0a142f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -124,6 +124,7 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ruleId: rule.id, ruleUuid: params.ruleId, ruleName: rule.name, + ruleRevision: rule.revision, ruleType: rule.ruleTypeId, spaceId, }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts index 9e5c383e825d1..631b67cd49601 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/index.test.ts @@ -26,6 +26,9 @@ describe('Security Telemetry filters', () => { 'event.provider': true, 'event.type': true, 'powershell.file.script_block_text': true, + 'kubernetes.pod.uid': true, + 'kubernetes.pod.name': true, + 'kubernetes.pod.ip': true, package_version: true, }; @@ -177,5 +180,23 @@ describe('Security Telemetry filters', () => { package_version: '3.4.1', }); }); + + it('copies over kubernetes fields', () => { + const event = { + not_event: 'much data, much wow', + 'event.id': '36857486973080746231799376445175633955031786243637182487', + 'event.ingested': 'May 17, 2022 @ 00:22:07.000', + 'kubernetes.pod.uid': '059a3767-7492-4fb5-92d4-93f458ddab44', + 'kubernetes.pod.name': 'kube-dns-6f4fd4zzz-7z7xj', + 'kubernetes.pod.ip': '10-245-0-5', + }; + expect(copyAllowlistedFields(allowlist, event)).toStrictEqual({ + 'event.id': '36857486973080746231799376445175633955031786243637182487', + 'event.ingested': 'May 17, 2022 @ 00:22:07.000', + 'kubernetes.pod.uid': '059a3767-7492-4fb5-92d4-93f458ddab44', + 'kubernetes.pod.name': 'kube-dns-6f4fd4zzz-7z7xj', + 'kubernetes.pod.ip': '10-245-0-5', + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts index 225206cca4b0d..42235cae66574 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filterlists/prebuilt_rules_alerts.ts @@ -215,6 +215,9 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { target_resources: true, }, }, + properties: { + category: true, + }, signinlogs: { properties: { app_display_name: true, @@ -253,6 +256,85 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { setting: { name: true, }, + application: { + name: true, + }, + old_value: true, + role: { + name: true, + }, + }, + event: { + type: true, + }, + }, + // kubernetes + kubernetes: { + audit: { + annotations: true, + verb: true, + user: { + groups: true, + }, + impersonatedUser: { + groups: true, + }, + objectRef: { + name: true, + namespace: true, + resource: true, + subresource: true, + }, + requestObject: { + spec: { + containers: { + image: true, + securityContext: { + allowPrivilegeEscalation: true, + capabilities: { + add: true, + }, + privileged: true, + procMount: true, + runAsGroup: true, + runAsUser: true, + }, + }, + hostIPC: true, + hostNetwork: true, + hostPID: true, + securityContext: { + runAsGroup: true, + runAsUser: true, + }, + serviceAccountName: true, + type: true, + volumes: { + hostPath: { + path: true, + }, + }, + }, + }, + requestURI: true, + responseObject: { + roleRef: { + kind: true, + resourceName: true, + }, + rules: true, + spec: { + containers: { + securityContext: { + allowPrivilegeEscalation: true, + }, + }, + }, + }, + responseStatus: { + code: true, + }, + userAgent: true, }, }, // office 360 @@ -275,6 +357,11 @@ export const prebuiltRuleAllowlistFields: AllowlistFields = { Enabled: true, ForwardAsAttachmentTo: true, ForwardTo: true, + ModifiedProperties: { + Role_DisplayName: { + NewValue: true, + }, + }, RedirectTo: true, }, }, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts index 4e3db8c657e04..e01eb21cbe68c 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts @@ -25,6 +25,7 @@ import { tlog, setIsElasticCloudDeployment, createTaskMetric, + processK8sUsernames, } from './helpers'; import type { ESClusterInfo, ESLicense, ExceptionListItem } from './types'; import type { PolicyConfig, PolicyData } from '../../../common/endpoint/types'; @@ -963,6 +964,7 @@ describe.skip('test create task metrics', () => { passed: true, }); }); + test('can succeed when error given', async () => { const stubTaskName = 'test'; const stubPassed = false; @@ -982,3 +984,95 @@ describe.skip('test create task metrics', () => { }); }); }); + +describe('Pii is removed from a kubernetes prebuilt rule alert', () => { + test('a document without the sensitive values is ignored', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + audit: {}, + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + }, + powershell: { + command_line: 'test', + module: 'test', + module_loaded: 'test', + module_version: 'test', + process_name: 'test', + }, + }; + + const ignoredDocument = processK8sUsernames(clusterUuid, testDocument); + expect(ignoredDocument).toEqual(testDocument); + }); + + test('kubernetes system usernames are not sanitized from a document', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + audit: { + user: { + username: 'system:serviceaccount:default:default', + groups: [ + 'system:serviceaccounts', + 'system:serviceaccounts:default', + 'system:authenticated', + ], + }, + impersonated_user: { + username: 'system:serviceaccount:default:default', + groups: [ + 'system:serviceaccounts', + 'system:serviceaccounts:default', + 'system:authenticated', + ], + }, + }, + }, + }; + + const sanitizedDocument = processK8sUsernames(clusterUuid, testDocument); + expect(sanitizedDocument).toEqual(testDocument); + }); + + test('kubernetes system usernames are sanitized from a document when not system users', async () => { + const clusterUuid = '7c5f1d31-ce87-4090-8dbf-decaac0261ca'; + const testDocument = { + kubernetes: { + pod: { + uid: 'test', + name: 'test', + ip: 'test', + labels: 'test', + annotations: 'test', + }, + audit: { + user: { + username: 'user1', + groups: ['group1', 'group2', 'group3'], + }, + impersonated_user: { + username: 'impersonatedUser1', + groups: ['group4', 'group5', 'group6'], + }, + }, + }, + }; + + const sanitizedDocument = processK8sUsernames(clusterUuid, testDocument); + expect(sanitizedDocument).toEqual(testDocument); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index f03621899c800..f5d6bc41ee349 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -8,8 +8,9 @@ import moment from 'moment'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import type { PackagePolicy } from '@kbn/fleet-plugin/common/types/models/package_policy'; -import { merge } from 'lodash'; +import { merge, set } from 'lodash'; import type { Logger } from '@kbn/core/server'; +import { sha256 } from 'js-sha256'; import { copyAllowlistedFields, filterList } from './filterlists'; import type { PolicyConfig, PolicyData } from '../../../common/endpoint/types'; import type { @@ -300,3 +301,47 @@ export const createTaskMetric = ( error_message: errorMessage, }; }; + +function obfuscateString(clusterId: string, toHash: string): string { + const valueToObfuscate = toHash + clusterId; + return sha256.create().update(valueToObfuscate).hex(); +} + +function isAllowlistK8sUsername(username: string) { + return ( + username === 'edit' || + username === 'view' || + username === 'admin' || + username === 'elastic-agent' || + username === 'cluster-admin' || + username.startsWith('system') + ); +} + +export const processK8sUsernames = (clusterId: string, event: TelemetryEvent): TelemetryEvent => { + // if there is no kubernetes key, return the event as is + if (event.kubernetes === undefined && event.kubernetes === null) { + return event; + } + + const username = event?.kubernetes?.audit?.user?.username; + const impersonatedUser = event?.kubernetes?.audit?.impersonated_user?.username; + + if (username !== undefined && username !== null && !isAllowlistK8sUsername(username)) { + set(event, 'kubernetes.audit.user.username', obfuscateString(clusterId, username)); + } + + if ( + impersonatedUser !== undefined && + impersonatedUser !== null && + !isAllowlistK8sUsername(impersonatedUser) + ) { + set( + event, + 'kubernetes.audit.impersonated_user.username', + obfuscateString(clusterId, impersonatedUser) + ); + } + + return event; +}; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts index a7fab953dad38..0fdc6cf32a69c 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/prebuilt_rule_alerts.ts @@ -11,7 +11,7 @@ import type { ITelemetryReceiver } from '../receiver'; import type { ESClusterInfo, ESLicense, TelemetryEvent } from '../types'; import type { TaskExecutionPeriod } from '../task'; import { TELEMETRY_CHANNEL_DETECTION_ALERTS, TASK_METRICS_CHANNEL } from '../constants'; -import { batchTelemetryRecords, tlog, createTaskMetric } from '../helpers'; +import { batchTelemetryRecords, createTaskMetric, processK8sUsernames, tlog } from '../helpers'; import { copyAllowlistedFields, filterList } from '../filterlists'; export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: number) { @@ -70,7 +70,12 @@ export function createTelemetryPrebuiltRuleAlertsTaskConfig(maxTelemetryBatch: n copyAllowlistedFields(filterList.prebuiltRulesAlerts, event) ); - const enrichedAlerts = processedAlerts.map( + const sanitizedAlerts = processedAlerts.map( + (event: TelemetryEvent): TelemetryEvent => + processK8sUsernames(clusterInfo?.cluster_uuid, event) + ); + + const enrichedAlerts = sanitizedAlerts.map( (event: TelemetryEvent): TelemetryEvent => ({ ...event, licence_id: licenseInfo?.uid, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index ba61f6b85aaab..df3b571714b29 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -64,6 +64,19 @@ export interface TelemetryEvent { id?: string; kind?: string; }; + kubernetes?: { + audit?: { + user?: { + username?: string; + groups?: string[]; + }; + impersonated_user?: { + username?: string; + groups?: string[]; + }; + pod?: SearchTypes; + }; + }; } // EP Policy Response diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index ab5ac00454a5e..e6ad26f1f405f 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -472,7 +472,6 @@ export class Plugin implements ISecuritySolutionPlugin { plugins.fleet.packagePolicyService, core.savedObjects, core.elasticsearch, - plugins.cloud, logger ); this.policyWatcher.start(licenseService); diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 622785a43b4e8..6b14dd7b616bf 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -23,7 +23,9 @@ ], "kbn_references": [ "@kbn/core", - { "path": "../../../src/setup_node_env/tsconfig.json" }, + { + "path": "../../../src/setup_node_env/tsconfig.json" + }, "@kbn/data-plugin", "@kbn/embeddable-plugin", "@kbn/files-plugin", @@ -153,5 +155,6 @@ "@kbn/security-solution-side-nav", "@kbn/core-lifecycle-browser", "@kbn/ecs", + "@kbn/url-state" ] } diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts index 8b1679241d9c9..fded7e8ee37b9 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.test.ts @@ -713,10 +713,10 @@ async function invokeExecutor({ tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts index 92664d05ff9ab..b424fb742b5ab 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.test.ts @@ -203,10 +203,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, @@ -270,10 +270,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, @@ -337,10 +337,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, @@ -403,10 +403,10 @@ describe('ruleType', () => { tags: [], consumer: '', producer: '', + revision: 0, ruleTypeId: '', ruleTypeName: '', enabled: true, - revision: 0, schedule: { interval: '1h', }, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx index f5b9602923dc4..bd5c46810d202 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/fields/source_field.tsx @@ -5,16 +5,9 @@ * 2.0. */ import React, { useEffect, useState } from 'react'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; -import { - EuiTabbedContent, - EuiFormRow, - EuiBetaBadge, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiTabbedContent, EuiFormRow } from '@elastic/eui'; import { CodeEditor } from './code_editor'; import { ScriptRecorderFields } from './script_recorder_fields'; import { ConfigKey, MonacoEditorLangId } from '../types'; @@ -51,40 +44,16 @@ export const SourceField = ({ onChange, onBlur, value, isEditFlow = false }: Sou const allTabs = [ { id: 'syntheticsBrowserScriptRecorderConfig', - name: ( - - - {isEditFlow ? ( - - ) : ( - - )} - - - - - + name: isEditFlow ? ( + + ) : ( + ), 'data-test-subj': 'syntheticsSourceTab__scriptRecorder', content: ( @@ -171,9 +140,3 @@ export const SourceField = ({ onChange, onBlur, value, isEditFlow = false }: Sou /> ); }; - -const StyledBetaBadgeWrapper = styled(EuiFlexItem)` - .euiToolTipAnchor { - display: flex; - } -`; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx index 0980ef1aa961d..b2839da207ff8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx @@ -118,7 +118,7 @@ export const MONITOR_TYPE_CONFIG = { ), link: '#', icon: 'videoPlayer', - beta: true, + beta: false, }, [FormMonitorType.SINGLE]: { id: 'syntheticsMonitorTypeSingle', @@ -142,7 +142,7 @@ export const MONITOR_TYPE_CONFIG = { ), link: '#', icon: 'videoPlayer', - beta: true, + beta: false, }, [FormMonitorType.HTTP]: { id: 'syntheticsMonitorTypeHTTP', diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx similarity index 74% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx index 9fd18e5dfa04d..d8638c4b9ed92 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/hooks/use_monitor_save.tsx @@ -6,8 +6,9 @@ */ import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; +import { toMountPoint, useKibana } from '@kbn/kibana-react-plugin/public'; import { useParams, useRouteMatch } from 'react-router-dom'; -import { useEffect } from 'react'; +import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { MONITOR_EDIT_ROUTE } from '../../../../../../common/constants'; @@ -18,6 +19,8 @@ import { cleanMonitorListState } from '../../../state'; import { useSyntheticsRefreshContext } from '../../../contexts'; export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonitor }) => { + const core = useKibana(); + const theme$ = core.services.theme?.theme$; const dispatch = useDispatch(); const { refreshApp } = useSyntheticsRefreshContext(); const { monitorId } = useParams<{ monitorId: string }>(); @@ -51,10 +54,16 @@ export const useMonitorSave = ({ monitorData }: { monitorData?: SyntheticsMonito dispatch(cleanMonitorListState()); kibanaService.toasts.addSuccess({ title: monitorId ? MONITOR_UPDATED_SUCCESS_LABEL : MONITOR_SUCCESS_LABEL, + text: toMountPoint( +

+ {monitorId ? MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT : MONITOR_SUCCESS_LABEL_SUBTEXT} +

, + { theme$ } + ), toastLifeTimeMs: 3000, }); } - }, [data, status, monitorId, loading, refreshApp, dispatch]); + }, [data, status, monitorId, loading, refreshApp, dispatch, theme$]); return { status, loading, isEdit }; }; @@ -66,6 +75,13 @@ const MONITOR_SUCCESS_LABEL = i18n.translate( } ); +const MONITOR_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorAddedSuccessMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); + const MONITOR_UPDATED_SUCCESS_LABEL = i18n.translate( 'xpack.synthetics.monitorManagement.monitorEditedSuccessMessage', { @@ -79,3 +95,10 @@ const MONITOR_FAILURE_LABEL = i18n.translate( defaultMessage: 'Monitor was unable to be saved. Please try again later.', } ); + +const MONITOR_UPDATED_SUCCESS_LABEL_SUBTEXT = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorFailureMessage.subtext', + { + defaultMessage: 'It will next run according to its defined schedule.', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx index 00c6e9b555727..885f44eaa5ae1 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/disabled_callout.tsx @@ -6,48 +6,36 @@ */ import React from 'react'; -import { EuiButton, EuiCallOut, EuiLink } from '@elastic/eui'; -import { InvalidApiKeyCalloutCallout } from './invalid_api_key_callout'; +import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; import * as labels from './labels'; import { useEnablement } from '../../../hooks'; export const DisabledCallout = ({ total }: { total: number }) => { - const { enablement, enableSynthetics, invalidApiKeyError, loading } = useEnablement(); + const { enablement, invalidApiKeyError, loading } = useEnablement(); const showDisableCallout = !enablement.isEnabled && total > 0; - const showInvalidApiKeyError = invalidApiKeyError && total > 0; + const showInvalidApiKeyCallout = invalidApiKeyError && total > 0; - if (showInvalidApiKeyError) { - return ; - } - - if (!showDisableCallout) { + if (!showDisableCallout && !showInvalidApiKeyCallout) { return null; } - return ( - -

{labels.CALLOUT_MANAGEMENT_DESCRIPTION}

- {enablement.canEnable || loading ? ( - { - enableSynthetics(); - }} - isLoading={loading} - > - {labels.SYNTHETICS_ENABLE_LABEL} - - ) : ( + return !enablement.canEnable && !loading ? ( + <> + +

{labels.CALLOUT_MANAGEMENT_DESCRIPTION}

{labels.CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} - + {labels.LEARN_MORE_LABEL}

- )} -
- ); +
+ + + ) : null; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx deleted file mode 100644 index 8361df0588c04..0000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/invalid_api_key_callout.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useEnablement } from '../../../hooks'; - -export const InvalidApiKeyCalloutCallout = () => { - const { enablement, enableSynthetics, loading } = useEnablement(); - - return ( - <> - -

{CALLOUT_MANAGEMENT_DESCRIPTION}

- {enablement.canEnable || loading ? ( - { - enableSynthetics(); - }} - isLoading={loading} - > - {SYNTHETICS_ENABLE_LABEL} - - ) : ( -

- {CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} - - {LEARN_MORE_LABEL} - -

- )} -
- - - ); -}; - -const LEARN_MORE_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.invalidKey', - { - defaultMessage: 'Learn more', - } -); - -const API_KEY_MISSING = i18n.translate('xpack.synthetics.monitorManagement.callout.apiKeyMissing', { - defaultMessage: 'Monitor Management is currently disabled because of missing API key', -}); - -const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( - 'xpack.synthetics.monitorManagement.callout.disabledCallout.invalidKey', - { - defaultMessage: 'Contact your administrator to enable Monitor Management.', - } -); - -const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( - 'xpack.synthetics.monitorManagement.callout.description.invalidKey', - { - defaultMessage: `Monitor Management is currently disabled. To run your monitors in one of Elastic's global managed testing locations, you need to re-enable monitor management.`, - } -); - -const SYNTHETICS_ENABLE_LABEL = i18n.translate( - 'xpack.synthetics.monitorManagement.syntheticsEnableLabel.invalidKey', - { - defaultMessage: 'Enable monitor management', - } -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts index d7f3f892c0c76..ff297267dcb62 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts @@ -24,14 +24,14 @@ export const LEARN_MORE_LABEL = i18n.translate( export const CALLOUT_MANAGEMENT_DISABLED = i18n.translate( 'xpack.synthetics.monitorManagement.callout.disabled', { - defaultMessage: 'Monitor Management is disabled', + defaultMessage: 'Monitor Management is currently disabled', } ); export const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( 'xpack.synthetics.monitorManagement.callout.disabled.adminContact', { - defaultMessage: 'Please contact your administrator to enable Monitor Management.', + defaultMessage: 'Monitor Management will be enabled when an admin visits the Synthetics app.', } ); @@ -39,7 +39,7 @@ export const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( 'xpack.synthetics.monitorManagement.callout.description.disabled', { defaultMessage: - 'Monitor Management is currently disabled. To run your monitors on Elastic managed Synthetics service, enable Monitor Management. Your existing monitors are paused.', + "Monitor Management requires a valid API key to run your monitors on Elastic's global managed testing locations. If you already had enabled Monitor Management previously, the API key may no longer be valid.", } ); @@ -64,14 +64,6 @@ export const ERROR_HEADING_LABEL = i18n.translate( } ); -export const BETA_TOOLTIP_MESSAGE = i18n.translate( - 'xpack.synthetics.monitors.management.betaLabel', - { - defaultMessage: - 'This functionality is in beta and is subject to change. The design and code is less mature than official generally available features and is being provided as-is with no warranties. Beta features are not subject to the support service level agreement of official generally available features.', - } -); - export const SUMMARY_LABEL = i18n.translate('xpack.synthetics.monitorManagement.summary.heading', { defaultMessage: 'Summary', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx index 8fd9f969d8e98..d4f30cb75236c 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx @@ -7,19 +7,12 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -import { BETA_TOOLTIP_MESSAGE } from '../labels'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; export const MonitorsPageHeader = () => ( - -
- -
-
); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx index b1efd88590588..e4fcee12f65a5 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx @@ -6,16 +6,16 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiTitle, EuiLink } from '@elastic/eui'; import { useEnablement } from '../../../../hooks/use_enablement'; import { kibanaService } from '../../../../../../utils/kibana_service'; import * as labels from './labels'; export const EnablementEmptyState = () => { - const { error, enablement, enableSynthetics, loading } = useEnablement(); + const { error, enablement, loading } = useEnablement(); const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); const [isEnabling, setIsEnabling] = useState(false); - const { isEnabled, canEnable } = enablement; + const { isEnabled } = enablement; const isEnabledRef = useRef(isEnabled); const buttonRef = useRef(null); @@ -44,11 +44,6 @@ export const EnablementEmptyState = () => { } }, [isEnabled, isEnabling, error]); - const handleEnableSynthetics = () => { - enableSynthetics(); - setIsEnabling(true); - }; - useEffect(() => { if (shouldFocusEnablementButton) { buttonRef.current?.focus(); @@ -57,33 +52,8 @@ export const EnablementEmptyState = () => { return !isEnabled && !loading ? ( - {canEnable - ? labels.MONITOR_MANAGEMENT_ENABLEMENT_LABEL - : labels.SYNTHETICS_APP_DISABLED_LABEL} - - } - body={ -

- {canEnable - ? labels.MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE - : labels.MONITOR_MANAGEMENT_DISABLED_MESSAGE} -

- } - actions={ - canEnable ? ( - - {labels.MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL} - - ) : null - } + title={

{labels.SYNTHETICS_APP_DISABLED_LABEL}

} + body={

{labels.MONITOR_MANAGEMENT_DISABLED_MESSAGE}

} footer={ <> @@ -91,7 +61,7 @@ export const EnablementEmptyState = () => { {labels.DOCS_LABEL} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx index a98f78249adbe..2e4eb6ca03a31 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx @@ -101,8 +101,8 @@ export const OverviewPage: React.FC = () => { return ( <> - + diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts index 394da8aefc086..fe726d0cbe3d2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts @@ -5,14 +5,9 @@ * 2.0. */ -import { useEffect, useCallback } from 'react'; +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { - getSyntheticsEnablement, - enableSynthetics, - disableSynthetics, - selectSyntheticsEnablement, -} from '../state'; +import { getSyntheticsEnablement, selectSyntheticsEnablement } from '../state'; export function useEnablement() { const dispatch = useDispatch(); @@ -35,7 +30,5 @@ export function useEnablement() { invalidApiKeyError: enablement ? !Boolean(enablement?.isValidApiKey) : false, error, loading, - enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]), - disableSynthetics: useCallback(() => dispatch(disableSynthetics()), [dispatch]), }; } diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts index 7369ce0917e5a..78c0d9484149e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts @@ -16,17 +16,3 @@ export const getSyntheticsEnablementSuccess = createAction( '[SYNTHETICS_ENABLEMENT] GET FAILURE' ); - -export const disableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] DISABLE'); -export const disableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] DISABLE SUCCESS'); -export const disableSyntheticsFailure = createAction( - '[SYNTHETICS_ENABLEMENT] DISABLE FAILURE' -); - -export const enableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] ENABLE'); -export const enableSyntheticsSuccess = createAction( - '[SYNTHETICS_ENABLEMENT] ENABLE SUCCESS' -); -export const enableSyntheticsFailure = createAction( - '[SYNTHETICS_ENABLEMENT] ENABLE FAILURE' -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts index 62b48676e3965..2e009cc0b89d2 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts @@ -14,17 +14,9 @@ import { apiService } from '../../../../utils/api_service'; export const fetchGetSyntheticsEnablement = async (): Promise => { - return await apiService.get( + return await apiService.put( API_URLS.SYNTHETICS_ENABLEMENT, undefined, MonitorManagementEnablementResultCodec ); }; - -export const fetchDisableSynthetics = async (): Promise<{}> => { - return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT); -}; - -export const fetchEnableSynthetics = async (): Promise => { - return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts index d3134c60f8fd3..14c912b07ce99 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts @@ -5,20 +5,15 @@ * 2.0. */ -import { takeLatest, takeLeading } from 'redux-saga/effects'; +import { takeLeading } from 'redux-saga/effects'; +import { i18n } from '@kbn/i18n'; import { getSyntheticsEnablement, getSyntheticsEnablementSuccess, getSyntheticsEnablementFailure, - disableSynthetics, - disableSyntheticsSuccess, - disableSyntheticsFailure, - enableSynthetics, - enableSyntheticsSuccess, - enableSyntheticsFailure, } from './actions'; -import { fetchGetSyntheticsEnablement, fetchDisableSynthetics, fetchEnableSynthetics } from './api'; import { fetchEffectFactory } from '../utils/fetch_effect'; +import { fetchGetSyntheticsEnablement } from './api'; export function* fetchSyntheticsEnablementEffect() { yield takeLeading( @@ -26,15 +21,13 @@ export function* fetchSyntheticsEnablementEffect() { fetchEffectFactory( fetchGetSyntheticsEnablement, getSyntheticsEnablementSuccess, - getSyntheticsEnablementFailure + getSyntheticsEnablementFailure, + undefined, + failureMessage ) ); - yield takeLatest( - disableSynthetics, - fetchEffectFactory(fetchDisableSynthetics, disableSyntheticsSuccess, disableSyntheticsFailure) - ); - yield takeLatest( - enableSynthetics, - fetchEffectFactory(fetchEnableSynthetics, enableSyntheticsSuccess, enableSyntheticsFailure) - ); } + +const failureMessage = i18n.translate('xpack.synthetics.settings.enablement.fail', { + defaultMessage: 'Failed to enable Monitor Management', +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts index 62cbce9bfe05b..26bf2b50b8325 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts @@ -9,12 +9,6 @@ import { createReducer } from '@reduxjs/toolkit'; import { getSyntheticsEnablement, getSyntheticsEnablementSuccess, - disableSynthetics, - disableSyntheticsSuccess, - disableSyntheticsFailure, - enableSynthetics, - enableSyntheticsSuccess, - enableSyntheticsFailure, getSyntheticsEnablementFailure, } from './actions'; import { MonitorManagementEnablementResult } from '../../../../../common/runtime_types'; @@ -45,39 +39,6 @@ export const syntheticsEnablementReducer = createReducer(initialState, (builder) .addCase(getSyntheticsEnablementFailure, (state, action) => { state.loading = false; state.error = action.payload; - }) - - .addCase(disableSynthetics, (state) => { - state.loading = true; - }) - .addCase(disableSyntheticsSuccess, (state, action) => { - state.loading = false; - state.error = null; - state.enablement = { - canEnable: state.enablement?.canEnable ?? false, - areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, - canManageApiKeys: state.enablement?.canManageApiKeys ?? false, - isEnabled: false, - isValidApiKey: true, - }; - }) - .addCase(disableSyntheticsFailure, (state, action) => { - state.loading = false; - state.error = action.payload; - }) - - .addCase(enableSynthetics, (state) => { - state.loading = true; - state.enablement = null; - }) - .addCase(enableSyntheticsSuccess, (state, action) => { - state.loading = false; - state.error = null; - state.enablement = action.payload; - }) - .addCase(enableSyntheticsFailure, (state, action) => { - state.loading = false; - state.error = action.payload; }); }); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts index 18ce74d9823bb..ae069537e1b26 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts +++ b/x-pack/plugins/synthetics/public/legacy_uptime/state/api/ml_anomaly.ts @@ -11,7 +11,7 @@ import { JobExistResult, MlCapabilitiesResponse, } from '@kbn/ml-plugin/public'; -import { extractErrorMessage } from '@kbn/ml-plugin/common'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import { apiService } from './utils'; import { AnomalyRecords, AnomalyRecordsParams } from '../actions'; import { API_URLS, ML_MODULE_ID } from '../../../../common/constants'; diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 370dd2e802bdf..0900b6f1685dc 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -213,11 +213,7 @@ export class UptimePlugin id: 'synthetics', euiIconType: 'logoObservability', order: 8400, - title: - PLUGIN.SYNTHETICS + - i18n.translate('xpack.synthetics.overview.headingBeta', { - defaultMessage: ' (beta)', - }), + title: PLUGIN.SYNTHETICS, category: DEFAULT_APP_CATEGORIES.observability, keywords: appKeywords, deepLinks: [], @@ -313,7 +309,6 @@ function registerUptimeRoutesWithNavigation( path: OVERVIEW_ROUTE, matchFullPath: false, ignoreTrailingSlash: true, - isBetaFeature: true, }, ], }, diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts index adab53c9d4268..3c62f99f7e67b 100644 --- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts +++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/service_api_key.ts @@ -72,7 +72,7 @@ const getSyntheticsServiceAPIKey = async (server: UptimeServerSetup) => { } }; -const setSyntheticsServiceApiKey = async ( +export const setSyntheticsServiceApiKey = async ( soClient: SavedObjectsClientContract, apiKey: SyntheticsServiceApiKey ) => { diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index 9e2038d05962a..836143d55f014 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -20,7 +20,6 @@ import { getServiceLocationsRoute } from './synthetics_service/get_service_locat import { deleteSyntheticsMonitorRoute } from './monitor_cruds/delete_monitor'; import { disableSyntheticsRoute, - enableSyntheticsRoute, getSyntheticsEnablementRoute, } from './synthetics_service/enablement'; import { @@ -61,7 +60,6 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ deleteSyntheticsMonitorProjectRoute, disableSyntheticsRoute, editSyntheticsMonitorRoute, - enableSyntheticsRoute, getServiceLocationsRoute, getSyntheticsMonitorRoute, getSyntheticsProjectMonitorsRoute, diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts index c4561f3ee9e00..87a10dbee9a8e 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/enablement.ts @@ -5,18 +5,12 @@ * 2.0. */ import { syntheticsServiceAPIKeySavedObject } from '../../legacy_uptime/lib/saved_objects/service_api_key'; -import { - SyntheticsRestApiRouteFactory, - UMRestApiRouteFactory, -} from '../../legacy_uptime/routes/types'; +import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes/types'; import { API_URLS } from '../../../common/constants'; -import { - generateAndSaveServiceAPIKey, - SyntheticsForbiddenError, -} from '../../synthetics_service/get_api_key'; +import { generateAndSaveServiceAPIKey } from '../../synthetics_service/get_api_key'; -export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ - method: 'GET', +export const getSyntheticsEnablementRoute: SyntheticsRestApiRouteFactory = (libs) => ({ + method: 'PUT', path: API_URLS.SYNTHETICS_ENABLEMENT, validate: {}, handler: async ({ savedObjectsClient, request, server }): Promise => { @@ -25,7 +19,18 @@ export const getSyntheticsEnablementRoute: UMRestApiRouteFactory = (libs) => ({ server, }); const { canEnable, isEnabled } = result; - if (canEnable && !isEnabled && server.config.service?.manifestUrl) { + const { security } = server; + const { apiKey, isValid } = await libs.requests.getAPIKeyForSyntheticsService({ + server, + }); + if (apiKey && !isValid) { + await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient); + await security.authc.apiKeys?.invalidateAsInternalUser({ + ids: [apiKey?.id || ''], + }); + } + const regenerationRequired = !isEnabled || !isValid; + if (canEnable && regenerationRequired && server.config.service?.manifestUrl) { await generateAndSaveServiceAPIKey({ request, authSavedObjectsClient: savedObjectsClient, @@ -68,7 +73,7 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ( server, }); await syntheticsServiceAPIKeySavedObject.delete(savedObjectsClient); - await security.authc.apiKeys?.invalidate(request, { ids: [apiKey?.id || ''] }); + await security.authc.apiKeys?.invalidateAsInternalUser({ ids: [apiKey?.id || ''] }); return response.ok({}); } catch (e) { server.logger.error(e); @@ -76,30 +81,3 @@ export const disableSyntheticsRoute: SyntheticsRestApiRouteFactory = (libs) => ( } }, }); - -export const enableSyntheticsRoute: UMRestApiRouteFactory = (libs) => ({ - method: 'POST', - path: API_URLS.SYNTHETICS_ENABLEMENT, - validate: {}, - handler: async ({ request, response, server, savedObjectsClient }): Promise => { - const { logger } = server; - try { - await generateAndSaveServiceAPIKey({ - request, - authSavedObjectsClient: savedObjectsClient, - server, - }); - return response.ok({ - body: await libs.requests.getSyntheticsEnablement({ - server, - }), - }); - } catch (e) { - logger.error(e); - if (e instanceof SyntheticsForbiddenError) { - return response.forbidden(); - } - throw e; - } - }, -}); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts index 4b15f4da43515..ca4a18e88d5d9 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.test.ts @@ -25,16 +25,6 @@ describe('getAPIKeyTest', function () { const logger = loggerMock.create(); - jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ - index: { - [syntheticsIndex]: { - auto_configure: true, - create_doc: true, - view_index_metadata: true, - }, - }, - } as any); - const server = { logger, security, @@ -52,6 +42,20 @@ describe('getAPIKeyTest', function () { encoded: '@#$%^&', }); + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ + index: { + [syntheticsIndex]: { + auto_configure: true, + create_doc: true, + view_index_metadata: true, + read: true, + }, + }, + } as any); + }); + it('should return existing api key', async () => { const getObject = jest .fn() @@ -79,4 +83,43 @@ describe('getAPIKeyTest', function () { 'ba997842-b0cf-4429-aa9d-578d9bf0d391' ); }); + + it('invalidates api keys with missing read permissions', async () => { + jest.spyOn(authUtils, 'checkHasPrivileges').mockResolvedValue({ + index: { + [syntheticsIndex]: { + auto_configure: true, + create_doc: true, + view_index_metadata: true, + read: false, + }, + }, + } as any); + + const getObject = jest + .fn() + .mockReturnValue({ attributes: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' } }); + + encryptedSavedObjects.getClient = jest.fn().mockReturnValue({ + getDecryptedAsInternalUser: getObject, + }); + const apiKey = await getAPIKeyForSyntheticsService({ + server, + }); + + expect(apiKey).toEqual({ + apiKey: { apiKey: 'qwerty', id: 'test', name: 'service-api-key' }, + isValid: false, + }); + + expect(encryptedSavedObjects.getClient).toHaveBeenCalledTimes(1); + expect(getObject).toHaveBeenCalledTimes(1); + expect(encryptedSavedObjects.getClient).toHaveBeenCalledWith({ + includedHiddenTypes: [syntheticsServiceApiKey.name], + }); + expect(getObject).toHaveBeenCalledWith( + 'uptime-synthetics-api-key', + 'ba997842-b0cf-4429-aa9d-578d9bf0d391' + ); + }); }); diff --git a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts index 0bc4f656901f4..79af4d6cfc718 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/get_api_key.ts @@ -56,7 +56,8 @@ export const getAPIKeyForSyntheticsService = async ({ const hasPermissions = indexPermissions.auto_configure && indexPermissions.create_doc && - indexPermissions.view_index_metadata; + indexPermissions.view_index_metadata && + indexPermissions.read; if (!hasPermissions) { return { isValid: false, apiKey }; @@ -92,6 +93,7 @@ export const generateAPIKey = async ({ } if (uptimePrivileges) { + /* Exposed to the user. Must create directly with the user */ return security.authc.apiKeys?.create(request, { name: 'synthetics-api-key (required for project monitors)', kibana_role_descriptors: { @@ -122,7 +124,8 @@ export const generateAPIKey = async ({ throw new SyntheticsForbiddenError(); } - return security.authc.apiKeys?.create(request, { + /* Not exposed to the user. May grant as internal user */ + return security.authc.apiKeys?.grantAsInternalUser(request, { name: 'synthetics-api-key (required for monitor management)', role_descriptors: { synthetics_writer: serviceApiKeyPrivileges, @@ -160,23 +163,24 @@ export const generateAndSaveServiceAPIKey = async ({ export const getSyntheticsEnablement = async ({ server }: { server: UptimeServerSetup }) => { const { security, config } = server; + const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ + getAPIKeyForSyntheticsService({ server }), + hasEnablePermissions(server), + security.authc.apiKeys.areAPIKeysEnabled(), + ]); + + const { canEnable, canManageApiKeys } = hasPrivileges; + if (!config.service?.manifestUrl) { return { canEnable: true, - canManageApiKeys: true, + canManageApiKeys, isEnabled: true, isValidApiKey: true, areApiKeysEnabled: true, }; } - const [apiKey, hasPrivileges, areApiKeysEnabled] = await Promise.all([ - getAPIKeyForSyntheticsService({ server }), - hasEnablePermissions(server), - security.authc.apiKeys.areAPIKeysEnabled(), - ]); - - const { canEnable, canManageApiKeys } = hasPrivileges; return { canEnable, canManageApiKeys, @@ -217,7 +221,7 @@ const hasEnablePermissions = async ({ uptimeEsClient }: UptimeServerSetup) => { return { canManageApiKeys, - canEnable: canManageApiKeys && hasClusterPermissions && hasIndexPermissions, + canEnable: hasClusterPermissions && hasIndexPermissions, }; }; diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts index 22ec8e8a3213e..2ed86eb91ae52 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.test.ts @@ -14,6 +14,24 @@ import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { ServiceConfig } from '../../common/config'; import axios from 'axios'; import { LocationStatus, PublicLocations } from '../../common/runtime_types'; +import { LicenseGetResponse } from '@elastic/elasticsearch/lib/api/types'; + +const licenseMock: LicenseGetResponse = { + license: { + status: 'active', + uid: '1d34eb9f-e66f-47d1-8d24-cd60d187587a', + type: 'trial', + issue_date: '2022-05-05T14:25:00.732Z', + issue_date_in_millis: 165176070074432, + expiry_date: '2022-06-04T14:25:00.732Z', + expiry_date_in_millis: 165435270073332, + max_nodes: 1000, + max_resource_units: null, + issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', + issuer: 'elasticsearch', + start_date_in_millis: -1, + }, +}; jest.mock('axios', () => jest.fn()); jest.mock('@kbn/server-http-tools', () => ({ @@ -167,7 +185,7 @@ describe('callAPI', () => { await apiClient.callAPI('POST', { monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(spy).toHaveBeenCalledTimes(3); @@ -181,7 +199,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', devUrl @@ -195,7 +213,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central_qa') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', 'https://qa.service.elstc.co' @@ -209,7 +227,7 @@ describe('callAPI', () => { monitor.locations.some((loc: any) => loc.id === 'us_central_staging') ), output, - licenseLevel: 'trial', + license: licenseMock.license, }, 'POST', 'https://qa.service.stg.co' @@ -223,6 +241,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -242,6 +261,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -261,6 +281,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { Authorization: 'Basic ZGV2OjEyMzQ1', @@ -324,7 +345,7 @@ describe('callAPI', () => { await apiClient.callAPI('POST', { monitors: testMonitors, output, - licenseLevel: 'platinum', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -333,7 +354,8 @@ describe('callAPI', () => { is_edit: undefined, output, stack_version: '8.7.0', - license_level: 'platinum', + license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', @@ -376,7 +398,7 @@ describe('callAPI', () => { await apiClient.runOnce({ monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -386,6 +408,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', @@ -428,7 +451,7 @@ describe('callAPI', () => { await apiClient.syncMonitors({ monitors: testMonitors, output, - licenseLevel: 'trial', + license: licenseMock.license, }); expect(axiosSpy).toHaveBeenNthCalledWith(1, { @@ -438,6 +461,7 @@ describe('callAPI', () => { output, stack_version: '8.7.0', license_level: 'trial', + license_issued_to: '2c515bd215ce444441f83ffd36a9d3d2546', }, headers: { 'x-kibana-version': '8.7.0', diff --git a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts index bfc872f3680a8..4fea1d04b64e4 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/service_api_client.ts @@ -11,6 +11,7 @@ import { catchError, tap } from 'rxjs/operators'; import * as https from 'https'; import { SslConfig } from '@kbn/server-http-tools'; import { Logger } from '@kbn/core/server'; +import { LicenseGetLicenseInformation } from '@elastic/elasticsearch/lib/api/types'; import { UptimeServerSetup } from '../legacy_uptime/lib/adapters'; import { sendErrorTelemetryEvents } from '../routes/telemetry/monitor_upgrade_sender'; import { MonitorFields, PublicLocations, ServiceLocationErrors } from '../../common/runtime_types'; @@ -27,7 +28,7 @@ export interface ServiceData { }; endpoint?: 'monitors' | 'runOnce' | 'sync'; isEdit?: boolean; - licenseLevel: string; + license: LicenseGetLicenseInformation; } export class ServiceAPIClient { @@ -142,7 +143,7 @@ export class ServiceAPIClient { async callAPI( method: 'POST' | 'PUT' | 'DELETE', - { monitors: allMonitors, output, endpoint, isEdit, licenseLevel }: ServiceData + { monitors: allMonitors, output, endpoint, isEdit, license }: ServiceData ) { if (this.username === TEST_SERVICE_USERNAME) { // we don't want to call service while local integration tests are running @@ -159,7 +160,7 @@ export class ServiceAPIClient { ); if (locMonitors.length > 0) { const promise = this.callServiceEndpoint( - { monitors: locMonitors, isEdit, endpoint, output, licenseLevel }, + { monitors: locMonitors, isEdit, endpoint, output, license }, method, url ); @@ -200,7 +201,7 @@ export class ServiceAPIClient { } async callServiceEndpoint( - { monitors, output, endpoint = 'monitors', isEdit, licenseLevel }: ServiceData, + { monitors, output, endpoint = 'monitors', isEdit, license }: ServiceData, method: 'POST' | 'PUT' | 'DELETE', baseUrl: string ) { @@ -233,7 +234,8 @@ export class ServiceAPIClient { output, stack_version: this.stackVersion, is_edit: isEdit, - license_level: licenseLevel, + license_level: license.type, + license_issued_to: license.issued_to, }, headers: authHeader, httpsAgent: this.getHttpsAgent(baseUrl), diff --git a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts index bd9c0032984c3..e4ef7cbbb6e67 100644 --- a/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/synthetics/server/synthetics_service/synthetics_service.ts @@ -307,7 +307,7 @@ export class SyntheticsService { this.syncErrors = await this.apiClient.post({ monitors, output, - licenseLevel: license.type, + license, }); } return this.syncErrors; @@ -329,7 +329,7 @@ export class SyntheticsService { monitors, output, isEdit, - licenseLevel: license.type, + license, }; this.syncErrors = await this.apiClient.put(data); @@ -372,7 +372,7 @@ export class SyntheticsService { service.syncErrors = await this.apiClient.syncMonitors({ monitors, output, - licenseLevel: license.type, + license, }); } catch (e) { sendErrorTelemetryEvents(service.logger, service.server.telemetry, { @@ -406,7 +406,7 @@ export class SyntheticsService { return await this.apiClient.runOnce({ monitors, output, - licenseLevel: license.type, + license, }); } catch (e) { this.logger.error(e); @@ -429,7 +429,7 @@ export class SyntheticsService { const data = { output, monitors: this.formatConfigs(configs), - licenseLevel: license.type, + license, }; return await this.apiClient.delete(data); } @@ -453,7 +453,7 @@ export class SyntheticsService { const data = { output, monitors, - licenseLevel: license.type, + license, }; return await this.apiClient.delete(data); } diff --git a/x-pack/plugins/synthetics/tsconfig.json b/x-pack/plugins/synthetics/tsconfig.json index a02e14d577b35..61b7fe8d8d19c 100644 --- a/x-pack/plugins/synthetics/tsconfig.json +++ b/x-pack/plugins/synthetics/tsconfig.json @@ -78,6 +78,7 @@ "@kbn/alerts-as-data-utils", "@kbn/exploratory-view-plugin", "@kbn/observability-shared-plugin", + "@kbn/ml-error-utils", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index f90faf53e87b5..25318fc9e2903 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -8,6 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { extractErrorMessage } from '@kbn/ml-error-utils'; import type { DeleteTransformStatus, DeleteTransformsRequestSchema, @@ -24,7 +25,6 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { const { http, data: { dataViews: dataViewsContract }, - ml: { extractErrorMessage }, application: { capabilities }, } = useAppDependencies(); const toastNotifications = useToastNotifications(); @@ -62,7 +62,7 @@ export const useDeleteIndexAndTargetIndex = (items: TransformListRow[]) => { ); } }, - [dataViewsContract, toastNotifications, extractErrorMessage] + [dataViewsContract, toastNotifications] ); const checkUserIndexPermission = useCallback(async () => { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx index c23d6ed475efc..e0a52978c0b4a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/source_search_bar/source_search_bar.tsx @@ -25,8 +25,8 @@ interface SourceSearchBarProps { } export const SourceSearchBar: FC = ({ dataView, searchBar }) => { const { - actions: { searchChangeHandler, searchSubmitHandler, setErrorMessage }, - state: { errorMessage, searchInput }, + actions: { searchChangeHandler, searchSubmitHandler, setQueryErrorMessage }, + state: { queryErrorMessage, searchInput }, } = searchBar; const { @@ -44,7 +44,7 @@ export const SourceSearchBar: FC = ({ dataView, searchBar return ( setErrorMessage(undefined)} + closePopover={() => setQueryErrorMessage(undefined)} input={ = ({ dataView, searchBar }} /> } - isOpen={errorMessage?.query === searchInput.query && errorMessage?.message !== ''} + isOpen={queryErrorMessage?.query === searchInput.query && queryErrorMessage?.message !== ''} > {i18n.translate('xpack.transform.stepDefineForm.invalidKuerySyntaxErrorMessageQueryBar', { - defaultMessage: 'Invalid query: {errorMessage}', + defaultMessage: 'Invalid query: {queryErrorMessage}', values: { - errorMessage: errorMessage?.message.split('\n')[0], + queryErrorMessage: queryErrorMessage?.message.split('\n')[0], }, })} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts index 775401decef35..157ab0051e631 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/index.ts @@ -13,4 +13,4 @@ export { getDefaultAggregationConfig } from './get_default_aggregation_config'; export { getDefaultGroupByConfig } from './get_default_group_by_config'; export { getDefaultStepDefineState } from './get_default_step_define_state'; export { getPivotDropdownOptions } from './get_pivot_dropdown_options'; -export type { ErrorMessage, Field, StepDefineExposedState } from './types'; +export type { Field, StepDefineExposedState } from './types'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index 682001a937381..2e66b2187a9e0 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -28,11 +28,6 @@ import { import { LatestFunctionConfig } from '../../../../../../../common/api_schemas/transforms'; import { RUNTIME_FIELD_TYPES } from '../../../../../../../common/shared_imports'; -export interface ErrorMessage { - query: string; - message: string; -} - export interface Field { name: EsFieldName; type: KBN_FIELD_TYPES | TIME_SERIES_METRIC_TYPES.COUNTER; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts index e8d56fc002981..62f8661f0ca49 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_search_bar.ts @@ -9,11 +9,11 @@ import { useState } from 'react'; import { toElasticsearchQuery, fromKueryExpression, luceneStringToDsl } from '@kbn/es-query'; import type { Query } from '@kbn/es-query'; +import type { QueryErrorMessage } from '@kbn/ml-error-utils'; import { getTransformConfigQuery } from '../../../../../common'; import { - ErrorMessage, StepDefineExposedState, QUERY_LANGUAGE_KUERY, QUERY_LANGUAGE_LUCENE, @@ -43,7 +43,9 @@ export const useSearchBar = ( const [searchQuery, setSearchQuery] = useState(defaults.searchQuery); - const [errorMessage, setErrorMessage] = useState(undefined); + const [queryErrorMessage, setQueryErrorMessage] = useState( + undefined + ); const searchChangeHandler = (query: Query) => setSearchInput(query); const searchSubmitHandler = (query: Query) => { @@ -61,7 +63,7 @@ export const useSearchBar = ( return; } } catch (e) { - setErrorMessage({ query: query.query as string, message: e.message }); + setQueryErrorMessage({ query: query.query as string, message: e.message }); } }; @@ -71,14 +73,14 @@ export const useSearchBar = ( actions: { searchChangeHandler, searchSubmitHandler, - setErrorMessage, + setQueryErrorMessage, setSearchInput, setSearchLanguage, setSearchQuery, setSearchString, }, state: { - errorMessage, + queryErrorMessage, transformConfigQuery, searchInput, searchLanguage, diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index 6f02b1978506f..9054ee7aa3b23 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -142,7 +142,7 @@ export function wrapEsError(err: any, statusCodeToMessageMap: Record = ({ throttle: frequency!.throttle, summary: frequency!.summary, }, - alertsFilter, + alerts_filter: alertsFilter, })), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts index 43beb66b40f9e..889f2634eacc9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rule_summary.test.ts @@ -28,6 +28,7 @@ describe('loadRuleSummary', () => { lastRun: '2021-04-01T22:18:27.609Z', muteAll: false, name: 'test', + revision: 0, ruleTypeId: '.index-threshold', status: 'OK', statusEndDate: '2021-04-01T22:19:25.174Z', @@ -55,6 +56,7 @@ describe('loadRuleSummary', () => { last_run: '2021-04-01T22:18:27.609Z', mute_all: false, name: 'test', + revision: 0, rule_type_id: '.index-threshold', status: 'OK', status_end_date: '2021-04-01T22:19:25.174Z', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts index 15097eb03f156..ee2418d22262b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/update.ts @@ -26,7 +26,7 @@ const rewriteBodyRequest: RewriteResponseCase = ({ actions, ... throttle: frequency!.throttle, summary: frequency!.summary, }, - alertsFilter, + alerts_filter: alertsFilter, ...(uuid && { uuid }), })), }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_query.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_query.tsx new file mode 100644 index 0000000000000..3cd22cc70a429 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_query.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { Filter } from '@kbn/es-query'; +import { i18n } from '@kbn/i18n'; +import { EuiSwitch, EuiSpacer } from '@elastic/eui'; +import { AlertsFilter } from '@kbn/alerting-plugin/common'; +import deepEqual from 'fast-deep-equal'; +import { AlertsSearchBar } from '../alerts_search_bar'; + +interface ActionAlertsFilterQueryProps { + state?: AlertsFilter['query']; + onChange: (update?: AlertsFilter['query']) => void; +} + +export const ActionAlertsFilterQuery: React.FC = ({ + state, + onChange, +}) => { + const [query, setQuery] = useState(state ?? { kql: '', filters: [] }); + + const queryEnabled = useMemo(() => Boolean(state), [state]); + + useEffect(() => { + const nextState = queryEnabled ? query : undefined; + if (!deepEqual(state, nextState)) onChange(nextState); + }, [queryEnabled, query, state, onChange]); + + const toggleQuery = useCallback( + () => onChange(state ? undefined : query), + [state, query, onChange] + ); + const updateQuery = useCallback( + (update: Partial) => { + setQuery({ + ...query, + ...update, + }); + }, + [query, setQuery] + ); + + const onQueryChange = useCallback( + ({ query: newQuery }) => updateQuery({ kql: newQuery }), + [updateQuery] + ); + + const onFiltersUpdated = useCallback( + (filters: Filter[]) => updateQuery({ filters }), + [updateQuery] + ); + + return ( + <> + + {queryEnabled && ( + <> + + + + )} + + ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx index a4c3edfda3344..ef73b3ab305c0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.test.tsx @@ -17,7 +17,7 @@ jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ })); describe('action_alerts_filter_timeframe', () => { - async function setup(timeframe: AlertsFilterTimeframe | null) { + async function setup(timeframe?: AlertsFilterTimeframe) { const wrapper = mountWithIntl( {}} /> ); @@ -32,7 +32,7 @@ describe('action_alerts_filter_timeframe', () => { } it('renders an unchecked switch when passed a null timeframe', async () => { - const wrapper = await setup(null); + const wrapper = await setup(); const alertsFilterTimeframeToggle = wrapper.find( '[data-test-subj="alertsFilterTimeframeToggle"]' diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx index a8e276f81535f..05fcd8fba77a7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_alerts_filter_timeframe.tsx @@ -24,8 +24,8 @@ import { AlertsFilterTimeframe, IsoWeekday } from '@kbn/alerting-plugin/common'; import { I18N_WEEKDAY_OPTIONS_DDD, ISO_WEEKDAYS } from '../../../common/constants'; interface ActionAlertsFilterTimeframeProps { - state: AlertsFilterTimeframe | null; - onChange: (update: AlertsFilterTimeframe | null) => void; + state?: AlertsFilterTimeframe; + onChange: (update?: AlertsFilterTimeframe) => void; } const TIMEZONE_OPTIONS = moment.tz?.names().map((n) => ({ label: n })) ?? [{ label: 'UTC' }]; @@ -46,14 +46,14 @@ const useDefaultTimezone = () => { return kibanaTz; }; -const useTimeframe = (initialTimeframe: AlertsFilterTimeframe | null) => { +const useTimeframe = (initialTimeframe?: AlertsFilterTimeframe) => { const timezone = useDefaultTimezone(); const DEFAULT_TIMEFRAME = { days: [], timezone, hours: { start: '00:00', - end: '24:00', + end: '23:59', }, }; return useState(initialTimeframe || DEFAULT_TIMEFRAME); @@ -79,12 +79,12 @@ export const ActionAlertsFilterTimeframe: React.FC { - const nextState = timeframeEnabled ? timeframe : null; + const nextState = timeframeEnabled ? timeframe : undefined; if (!deepEqual(state, nextState)) onChange(nextState); }, [timeframeEnabled, timeframe, state, onChange]); const toggleTimeframe = useCallback( - () => onChange(state ? null : timeframe), + () => onChange(state ? undefined : timeframe), [state, timeframe, onChange] ); const updateTimeframe = useCallback( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx index 18a8a577b7e0f..5be6a91698833 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.test.tsx @@ -351,7 +351,7 @@ describe('action_form', () => { (initialAlert.actions[index] = { ...initialAlert.actions[index], alertsFilter: { - ...(initialAlert.actions[index].alertsFilter ?? { query: null, timeframe: null }), + ...initialAlert.actions[index].alertsFilter, [key]: value, }, }) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx index 38d267d014824..1743d435b722f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_type_form.tsx @@ -61,6 +61,7 @@ import { ConnectorsSelection } from './connectors_selection'; import { ActionNotifyWhen } from './action_notify_when'; import { validateParamsForWarnings } from '../../lib/validate_params_for_warnings'; import { ActionAlertsFilterTimeframe } from './action_alerts_filter_timeframe'; +import { ActionAlertsFilterQuery } from './action_alerts_filter_query'; export type ActionTypeFormProps = { actionItem: RuleAction; @@ -405,8 +406,13 @@ export const ActionTypeForm = ({ {showActionAlertsFilter && ( <> {!hideNotifyWhen && } + setActionAlertsFilterProperty('query', query, index)} + /> + setActionAlertsFilterProperty('timeframe', timeframe, index)} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx index 41893e785f5bf..78039c753a276 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.test.tsx @@ -35,14 +35,14 @@ const renderWithSecretFields = ({ describe('EncryptedFieldsCallout', () => { const isCreateTests: Array<[number, string]> = [ - [1, 'Remember value label0. You must reenter it each time you edit the connector.'], + [1, 'Remember your label0 value. You must reenter it each time you edit the connector.'], [ 2, - 'Remember values label0 and label1. You must reenter them each time you edit the connector.', + 'Remember your label0 and label1 values. You must reenter them each time you edit the connector.', ], [ 3, - 'Remember values label0, label1, and label2. You must reenter them each time you edit the connector.', + 'Remember your label0, label1, and label2 values. You must reenter them each time you edit the connector.', ], ]; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx index 7bc0fdbc0703c..35cbb473c6a3f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/encrypted_fields_callout.tsx @@ -104,7 +104,7 @@ const EncryptedFieldsCalloutComponent: React.FC = ( { values: { secretFieldsLabel, encryptedFieldsLength: totalSecretFields }, defaultMessage: - 'Remember value{encryptedFieldsLength, plural, one {} other {s}} {secretFieldsLabel}. You must reenter {encryptedFieldsLength, plural, one {it} other {them}} each time you edit the connector.', + 'Remember your {secretFieldsLabel} {encryptedFieldsLength, plural, one {value} other {values}}. You must reenter {encryptedFieldsLength, plural, one {it} other {them}} each time you edit the connector.', } )} dataTestSubj="create-connector-secrets-callout" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index a03c34c90962e..9edde99574ca1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -234,6 +234,7 @@ const ActionsConnectorsList: React.FunctionComponent = () => { <> editItem(item, EditConnectorTabs.Configuration)} key={item.id} disabled={actionTypesIndex ? !actionTypesIndex[item.actionTypeId]?.enabled : true} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx index 6a170363d34a8..49b1f9905cd7f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/alerts_search_bar.tsx @@ -19,9 +19,16 @@ export function AlertsSearchBar({ appName, featureIds, query, + filters, onQueryChange, + onFiltersUpdated, rangeFrom, rangeTo, + showFilterBar = false, + showDatePicker = true, + showSubmitButton = true, + placeholder = SEARCH_BAR_PLACEHOLDER, + submitOnBlur = false, }: AlertsSearchBarProps) { const { unifiedSearch: { @@ -52,14 +59,20 @@ export function AlertsSearchBar({ ); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts index 2c94c250c168a..b7616333c199a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Filter } from '@kbn/es-query'; import { ValidFeatureId } from '@kbn/rule-data-utils'; export type QueryLanguageType = 'lucene' | 'kuery'; @@ -15,8 +16,15 @@ export interface AlertsSearchBarProps { rangeFrom?: string; rangeTo?: string; query?: string; + filters?: Filter[]; + showFilterBar?: boolean; + showDatePicker?: boolean; + showSubmitButton?: boolean; + placeholder?: string; + submitOnBlur?: boolean; onQueryChange: (query: { dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' }; query?: string; }) => void; + onFiltersUpdated?: (filters: Filter[]) => void; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx index f9d209549da0c..23fac59fca208 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/bulk_actions/bulk_actions.test.tsx @@ -691,7 +691,8 @@ describe('AlertsTable.BulkActions', () => { ).toBeTruthy(); }); - describe('and clear the selection is clicked', () => { + // FLAKY: https://github.com/elastic/kibana/issues/154970 + describe.skip('and clear the selection is clicked', () => { it('should turn off the toolbar', async () => { const props = { ...tablePropsWithBulkActions, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx index 0ec383c84f6e6..32e5c0e83d297 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/event_log/event_log_list_cell_renderer.test.tsx @@ -111,6 +111,16 @@ describe('rule_event_log_list_cell_renderer', () => { expect(wrapper.find(EuiIcon).props().color).toEqual('gray'); }); + it('renders maintenance window correctly', () => { + const wrapper = shallow( + + ); + expect(wrapper.text()).toEqual('test-id-1, test-id-2'); + }); + it('links to rules on the correct space', () => { const wrapper1 = shallow( {value ? 'true' : 'false'}; } + if (columnId === 'maintenance_window_ids') { + return <>{Array.isArray(value) ? value.join(', ') : ''}; + } + return <>{value}; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx index f3775e5de5066..821d4edf16c66 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.test.tsx @@ -481,6 +481,7 @@ function mockRuleSummary(overloads: Partial = {}): RuleSummary { throttle: '', enabled: true, errorMessages: [], + revision: 0, statusStartDate: fake2MinutesAgo.toISOString(), statusEndDate: fakeNow.toISOString(), alerts: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index 81c5e0d7d7e90..8b99cc910d8da 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -197,6 +197,7 @@ export function alertToListItem( isMuted, sortPriority, flapping: alert.flapping, + ...(alert.maintenanceWindowIds ? { maintenanceWindowIds: alert.maintenanceWindowIds } : {}), }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx index 3b3142181d8e7..5a6fad3b939a2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_alert_list.tsx @@ -91,6 +91,21 @@ const alertsTableColumns = ( width: '80px', 'data-test-subj': 'alertsTableCell-duration', }, + { + field: 'maintenanceWindowIds', + width: '250px', + render: (value: string[]) => { + return Array.isArray(value) ? value.join(', ') : ''; + }, + name: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.alertsList.columns.maintenanceWindowIds', + { + defaultMessage: 'Maintenance windows', + } + ), + sortable: false, + 'data-test-subj': 'alertsTableCell-maintenanceWindowIds', + }, { field: '', align: RIGHT_ALIGNMENT, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx index 7df89d799b2ae..7965f58fa8420 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_event_log_list_table.tsx @@ -610,6 +610,20 @@ export const RuleEventLogListTable = ( ), isSortable: getIsColumnSortable('timed_out'), }, + { + id: 'maintenance_window_ids', + displayAsText: i18n.translate( + 'xpack.triggersActionsUI.sections.ruleDetails.eventLogColumn.maintenanceWindowIds', + { + defaultMessage: 'Maintenance windows', + } + ), + actions: { + showSortAsc: false, + showSortDesc: false, + }, + isSortable: getIsColumnSortable('maintenance_window_ids'), + }, ], [getPaginatedRowIndex, onFlyoutOpen, onFilterChange, hasRuleNames, showFromAllSpaces, logs] ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx index 117bca134f0f9..1826894ef70f9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule_route.test.tsx @@ -162,6 +162,7 @@ function mockRuleSummary(overloads: Partial = {}): any { throttle: null, enabled: true, errorMessages: [], + revision: 0, statusStartDate: fake2MinutesAgo.toISOString(), statusEndDate: fakeNow.toISOString(), alerts: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts index 4146ac8006325..d72e381c170ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/test_helpers.ts @@ -95,6 +95,7 @@ export function mockRuleSummary(overloads: Partial = {}): RuleSumma throttle: '', enabled: true, errorMessages: [], + revision: 0, statusStartDate: '2022-03-21T07:40:46-07:00', statusEndDate: '2022-03-25T07:40:46-07:00', alerts: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts index c469b61690c3a..f66f648051715 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/types.ts @@ -14,4 +14,5 @@ export interface AlertListItem { isMuted: boolean; sortPriority: number; flapping: boolean; + maintenanceWindowIds?: string[]; } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts index 8ac7c38276233..54f3871928fb3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_reducer.ts @@ -13,6 +13,7 @@ import { IntervalSchedule, RuleActionAlertsFilterProperty, } from '@kbn/alerting-plugin/common'; +import { isEmpty } from 'lodash/fp'; import { Rule, RuleAction } from '../../../types'; import { DEFAULT_FREQUENCY } from '../../../common/constants'; @@ -237,12 +238,18 @@ export const ruleReducer = ( return state; } else { const oldAction = rule.actions.splice(index, 1)[0]; + const { alertsFilter, ...rest } = oldAction; + const updatedAlertsFilter = { ...alertsFilter }; + + if (value) { + updatedAlertsFilter[key] = value; + } else { + delete updatedAlertsFilter[key]; + } + const updatedAction = { - ...oldAction, - alertsFilter: { - ...(oldAction.alertsFilter ?? { timeframe: null, query: null }), - [key]: value, - }, + ...rest, + ...(!isEmpty(updatedAlertsFilter) ? { alertsFilter: updatedAlertsFilter } : {}), }; rule.actions.splice(index, 0, updatedAction); return { diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts index f1a67c568b67f..eee078591a3a7 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/slack_simulation.ts @@ -35,7 +35,7 @@ export async function initPlugin() { } // store a message that was posted to be remembered - const match = text.match(/^message (.*)$/); + const match = text.match(/^message ([\S\s]*)$/); if (match) { messages.push(match[1]); response.statusCode = 200; diff --git a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts index f3f0aa8f6469b..baa6ee80a8e53 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/actions_simulators/server/webhook_simulation.ts @@ -79,7 +79,7 @@ function createServerCallback() { } // store a payload that was posted to be remembered - const match = data.match(/^payload (.*)$/); + const match = data.match(/^payload ([\S\s]*)$/); if (match) { payloads.push(match[1]); response.statusCode = 200; @@ -89,6 +89,8 @@ function createServerCallback() { response.statusCode = 400; response.end(`unexpected body ${data}`); + // eslint-disable-next-line no-console + console.log(`webhook simulator received unexpected body: ${data}`); return; }); } else { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts index 60e7e82966864..6d716b5d3c235 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts @@ -47,6 +47,7 @@ export const DeepContextVariables = { arrayI: [44, 45], nullJ: null, undefinedK: undefined, + dateL: '2023-04-20T04:13:17.858Z', }; function getAlwaysFiringAlertType() { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts index f0ba9dc451937..7ad1a54de2485 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/get_alert_summary.ts @@ -78,6 +78,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo expect(stableBody).to.eql({ name: 'abc', tags: ['foo'], + revision: 0, rule_type_id: 'test.noop', consumer: 'alertsFixture', status: 'OK', 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 7b5f41f4448d2..410fee01c71f5 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 @@ -1244,8 +1244,7 @@ instanceStateValue: true throttle: null, summary: true, alertsFilter: { - timeframe: null, - query: { kql: 'kibana.alert.rule.name:abc' }, + query: { kql: 'kibana.alert.rule.name:abc', filters: [] }, }, }); @@ -1307,8 +1306,7 @@ instanceStateValue: true throttle: null, summary: true, alertsFilter: { - timeframe: null, - query: { kql: 'kibana.alert.instance.id:1' }, + query: { kql: 'kibana.alert.instance.id:1', filters: [] }, }, }); @@ -1383,7 +1381,6 @@ instanceStateValue: true timezone: 'UTC', hours: { start, end }, }, - query: null, }, }); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts index fac54e789bd30..4e3a3fb70a455 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/event_log.ts @@ -1293,7 +1293,12 @@ export default function eventLogTests({ getService }: FtrProviderContext) { }); }); - const actionsToCheck = ['new-instance', 'active-instance', 'recovered-instance']; + const actionsToCheck = [ + 'new-instance', + 'active-instance', + 'recovered-instance', + 'execute', + ]; events.forEach((event) => { if (actionsToCheck.includes(event?.event?.action || '')) { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts index 75fd92ec61eaa..9be9db54904aa 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/get_alert_summary.ts @@ -68,6 +68,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo id: createdRule.id, name: 'abc', tags: ['foo'], + revision: 0, rule_type_id: 'test.noop', consumer: 'alertsFixture', status: 'OK', @@ -106,6 +107,7 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo id: createdRule.id, name: 'abc', tags: ['foo'], + revision: 0, rule_type_id: 'test.noop', consumer: 'alertsFixture', status: 'OK', @@ -268,6 +270,94 @@ export default function createGetAlertSummaryTests({ getService }: FtrProviderCo expect(actualAlerts).to.eql(expectedAlerts); }); + it('handles multi-alert status during maintenance window', async () => { + // pattern of when the rule should fire + const pattern = { + alertA: [true, true, true, true], + alertB: [true, true, false, false], + alertC: [true, true, true, true], + }; + + const { body: createdRule } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestRuleData({ + rule_type_id: 'test.patternFiring', + params: { pattern }, + schedule: { interval: '1s' }, + }) + ) + .expect(200); + + objectRemover.add(Spaces.space1.id, createdRule.id, 'rule', 'alerting'); + + const { body: createdMaintenanceWindow } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send({ + title: 'test-maintenance-window', + duration: 60 * 60 * 1000, // 1 hr + r_rule: { + dtstart: new Date().toISOString(), + tzid: 'UTC', + freq: 2, // weekly + }, + }); + + objectRemover.add( + Spaces.space1.id, + createdMaintenanceWindow.id, + 'rules/maintenance_window', + 'alerting', + true + ); + + await alertUtils.muteInstance(createdRule.id, 'alertC'); + await alertUtils.muteInstance(createdRule.id, 'alertD'); + await waitForEvents(createdRule.id, ['new-instance', 'recovered-instance']); + const response = await supertest.get( + `${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rule/${createdRule.id}/_alert_summary` + ); + + const actualAlerts = checkAndCleanActualAlerts(response.body.alerts, [ + 'alertA', + 'alertB', + 'alertC', + ]); + + const expectedAlerts = { + alertA: { + status: 'Active', + muted: false, + actionGroupId: 'default', + activeStartDate: actualAlerts.alertA.activeStartDate, + flapping: false, + maintenanceWindowIds: [createdMaintenanceWindow.id], + }, + alertB: { + status: 'OK', + muted: false, + flapping: false, + maintenanceWindowIds: [createdMaintenanceWindow.id], + }, + alertC: { + status: 'Active', + muted: true, + actionGroupId: 'default', + activeStartDate: actualAlerts.alertC.activeStartDate, + flapping: false, + maintenanceWindowIds: [createdMaintenanceWindow.id], + }, + alertD: { + status: 'OK', + muted: true, + flapping: false, + }, + }; + expect(actualAlerts).to.eql(expectedAlerts); + }); + describe('legacy', () => { it('handles multi-alert status', async () => { // pattern of when the alert should fire diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts index 1eb7d99b93bda..764dd728b1347 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/mustache_templates.ts @@ -14,13 +14,16 @@ import http from 'http'; import getPort from 'get-port'; -import { URL, format as formatUrl } from 'url'; import axios from 'axios'; import expect from '@kbn/expect'; import { getWebhookServer, getSlackServer } from '@kbn/actions-simulators-plugin/server/plugin'; import { Spaces } from '../../../scenarios'; -import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib'; +import { + getUrlPrefix, + getTestRuleData as getCoreTestRuleData, + ObjectRemover, +} from '../../../../common/lib'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -32,8 +35,10 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon const objectRemover = new ObjectRemover(supertest); let webhookSimulatorURL: string = ''; let webhookServer: http.Server; + let webhookConnector: any; let slackSimulatorURL: string = ''; let slackServer: http.Server; + let slackConnector: any; before(async () => { let availablePort: number; @@ -42,6 +47,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon availablePort = await getPort({ port: 9000 }); webhookServer.listen(availablePort); webhookSimulatorURL = `http://localhost:${availablePort}`; + webhookConnector = await createWebhookConnector(webhookSimulatorURL); slackServer = await getSlackServer(); availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); @@ -49,6 +55,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon slackServer.listen(availablePort); } slackSimulatorURL = `http://localhost:${availablePort}`; + slackConnector = await createSlackConnector(slackSimulatorURL); }); after(async () => { @@ -57,219 +64,177 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon slackServer.close(); }); - it('should handle escapes in webhook', async () => { - const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing mustache escapes for webhook', - connector_type_id: '.webhook', - secrets: {}, - config: { - headers: { - 'Content-Type': 'text/plain', - }, - url, + describe('escaping', () => { + it('should handle escapes in webhook', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const EscapableStrings + const template = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}'; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be(`\\"double quote\\" -- line\\nfeed`); + }); + + it('should handle escapes in slack', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const EscapableStrings + const template = + '{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}'; + + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const EscapableStrings - const varsTemplate = '{{context.escapableDoubleQuote}} -- {{context.escapableLineFeed}}'; - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for webhook', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - body: `payload {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>"); + }); + + it('should handle context variable object expansion', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = '{{context.deep}}'; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be( + '{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null,"dateL":"2023-04-20T04:13:17.858Z"}' ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(webhookSimulatorURL, createdAlert.id) - ); - expect(body).to.be(`\\"double quote\\" -- line\\nfeed`); - }); - - it('should handle escapes in slack', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: "testing backtic'd mustache escapes for slack", - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + }); + + it('should render kibanaBaseUrl as empty string since not configured', async () => { + const template = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"'; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const EscapableStrings - const varsTemplate = - '{{context.escapableBacktic}} -- {{context.escapableBold}} -- {{context.escapableBackticBold}} -- {{context.escapableHtml}}'; + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be('kibanaBaseUrl: ""'); + }); - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for slack', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be("back'tic -- `*bold*` -- `'*bold*'` -- <&>"); - }); - - it('should handle context variable object expansion', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing context variable expansion', - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + it('should render action variables in rule action', async () => { + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, - // const DeepContextVariables - const varsTemplate = '{{context.deep}}'; - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing context variable expansion', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true, true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be( + `old id variable: ${rule.id}, new id variable: ${rule.id}, old name variable: ${rule.name}, new name variable: ${rule.name}` ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be( - '{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null}' - ); + }); }); - it('should render kibanaBaseUrl as empty string since not configured', async () => { - const actionResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) - .set('kbn-xsrf', 'test') - .send({ - name: 'testing context variable expansion', - connector_type_id: '.slack', - secrets: { - webhookUrl: slackSimulatorURL, + describe('lambdas', () => { + it('should handle ParseHjson', async () => { + const template = `{{#ParseHjson}} { + ruleId: {{rule.id}} + ruleName: {{rule.name}} + } {{/ParseHjson}}`; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); - - const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"'; + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + expect(body).to.be(`{"ruleId":"${rule.id}","ruleName":"testing mustache templates"}`); + }); + + it('should handle asJSON', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep.objectA}} + {{{arrayC}}} {{{arrayC.asJSON}}} + {{/context.deep.objectA}} + `; + const rule = await createRule({ + id: webhookConnector.id, + group: 'default', + params: { + body: `payload {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(webhookSimulatorURL, rule.id)); + const expected1 = `{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}`; + const expected2 = `[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}]`; + expect(body.trim()).to.be(`${expected1} ${expected2}`); + }); + + it('should handle EvalMath', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep}}avg({{arrayI.0}}, {{arrayI.1}})/100 => {{#EvalMath}} + round((arrayI[0] + arrayI[1]) / 2 / 100, 2) + {{/EvalMath}}{{/context.deep}}`; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body).to.be(`avg(44, 45)/100 => 0.45`); + }); + + it('should handle FormatDate', async () => { + // from x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts, + // const DeepContextVariables + const template = `{{#context.deep}}{{#FormatDate}} + {{{dateL}}} ; America/New_York; dddd MMM Do YYYY HH:mm:ss + {{/FormatDate}}{{/context.deep}}`; + const rule = await createRule({ + id: slackConnector.id, + group: 'default', + params: { + message: `message {{alertId}} - ${template}`, + }, + }); + const body = await retry.try(async () => waitForActionBody(slackSimulatorURL, rule.id)); + expect(body.trim()).to.be(`Thursday Apr 20th 2023 00:13:17`); + }); + }); - const alertResponse = await supertest + async function createRule(action: any) { + const ruleResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing context variable kibanaBaseUrl', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true, true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - message: `message {{alertId}} - ${varsTemplate}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(slackSimulatorURL, createdAlert.id) - ); - expect(body).to.be('kibanaBaseUrl: ""'); - }); + .send(getTestRuleData({ actions: [action] })); + expect(ruleResponse.status).to.eql(200); + const rule = ruleResponse.body; + objectRemover.add(Spaces.space1.id, rule.id, 'rule', 'alerting'); + + return rule; + } - it('should render action variables in rule action', async () => { - const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); - const actionResponse = await supertest + async function createWebhookConnector(url: string) { + const createResponse = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) .set('kbn-xsrf', 'test') .send({ - name: 'testing action variable rendering', + name: 'testing mustache for webhook', connector_type_id: '.webhook', secrets: {}, config: { @@ -279,42 +244,30 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon url, }, }); - expect(actionResponse.status).to.eql(200); - const createdAction = actionResponse.body; - objectRemover.add(Spaces.space1.id, createdAction.id, 'connector', 'actions'); + expect(createResponse.status).to.eql(200); + const connector = createResponse.body; + objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions'); - const alertResponse = await supertest - .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) - .set('kbn-xsrf', 'foo') - .send( - getTestRuleData({ - name: 'testing variable escapes for webhook', - rule_type_id: 'test.patternFiring', - params: { - pattern: { instance: [true] }, - }, - actions: [ - { - id: createdAction.id, - group: 'default', - params: { - body: `payload {{rule.id}} - old id variable: {{alertId}}, new id variable: {{rule.id}}, old name variable: {{alertName}}, new name variable: {{rule.name}}`, - }, - }, - ], - }) - ); - expect(alertResponse.status).to.eql(200); - const createdAlert = alertResponse.body; - objectRemover.add(Spaces.space1.id, createdAlert.id, 'rule', 'alerting'); - - const body = await retry.try(async () => - waitForActionBody(webhookSimulatorURL, createdAlert.id) - ); - expect(body).to.be( - `old id variable: ${createdAlert.id}, new id variable: ${createdAlert.id}, old name variable: ${createdAlert.name}, new name variable: ${createdAlert.name}` - ); - }); + return connector; + } + + async function createSlackConnector(url: string) { + const createResponse = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'test') + .send({ + name: 'testing mustache for slack', + connector_type_id: '.slack', + secrets: { + webhookUrl: url, + }, + }); + expect(createResponse.status).to.eql(200); + const connector = createResponse.body; + objectRemover.add(Spaces.space1.id, connector.id, 'connector', 'actions'); + + return connector; + } }); async function waitForActionBody(url: string, id: string): Promise { @@ -322,7 +275,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon expect(response.status).to.eql(200); for (const datum of response.data) { - const match = datum.match(/^(.*) - (.*)$/); + const match = datum.match(/^(.*) - ([\S\s]*)$/); if (match == null) continue; if (match[1] === id) return match[2]; @@ -331,3 +284,15 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon throw new Error(`no action body posted yet for id ${id}`); } } + +function getTestRuleData(overrides: any) { + const defaults = { + name: 'testing mustache templates', + rule_type_id: 'test.patternFiring', + params: { + pattern: { instance: [true] }, + }, + }; + + return getCoreTestRuleData({ ...overrides, ...defaults }); +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/infra.ts b/x-pack/test/api_integration/apis/metrics_ui/infra.ts index 4749492982b9d..5d84ba1798a9a 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/infra.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/infra.ts @@ -89,6 +89,7 @@ export default function ({ getService }: FtrProviderContext) { metadata: [ { name: 'host.os.name', value: 'CentOS Linux' }, { name: 'cloud.provider', value: 'gcp' }, + { name: 'host.ip', value: null }, ], metrics: [ { name: 'cpu', value: 0.44708333333333333 }, @@ -120,6 +121,7 @@ export default function ({ getService }: FtrProviderContext) { metadata: [ { name: 'host.os.name', value: 'CentOS Linux' }, { name: 'cloud.provider', value: 'gcp' }, + { name: 'host.ip', value: null }, ], metrics: [{ name: 'memory', value: 0.4563333333333333 }], name: 'gke-observability-8--observability-8--bc1afd95-f0zc', @@ -128,6 +130,7 @@ export default function ({ getService }: FtrProviderContext) { metadata: [ { name: 'host.os.name', value: 'CentOS Linux' }, { name: 'cloud.provider', value: 'gcp' }, + { name: 'host.ip', value: null }, ], metrics: [{ name: 'memory', value: 0.32066666666666666 }], name: 'gke-observability-8--observability-8--bc1afd95-ngmh', @@ -136,6 +139,7 @@ export default function ({ getService }: FtrProviderContext) { metadata: [ { name: 'host.os.name', value: 'CentOS Linux' }, { name: 'cloud.provider', value: 'gcp' }, + { name: 'host.ip', value: null }, ], metrics: [{ name: 'memory', value: 0.2346666666666667 }], name: 'gke-observability-8--observability-8--bc1afd95-nhhw', diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts index b1bc58abe7113..e6a8ae14cda1a 100644 --- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts +++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts @@ -74,7 +74,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); await supertest .post('/api/fleet/epm/packages/synthetics/0.11.4') diff --git a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts index f20a2cdf61a45..bf4447a1b5969 100644 --- a/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/edit_monitor.ts @@ -43,7 +43,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { _httpMonitorJson = getFixtureJson('http_monitor'); await supertest.post('/api/fleet/setup').set('kbn-xsrf', 'true').send().expect(200); - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); const testPolicyName = 'Fleet test server policy' + Date.now(); const apiResponse = await testPrivateLocations.addFleetPolicy(testPolicyName); diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts index 5394ca64545e6..00772c5550ac1 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor.ts @@ -32,7 +32,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); _monitors = [ getFixtureJson('icmp_monitor'), diff --git a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts index 25aad0704cddd..625dbdac61608 100644 --- a/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts +++ b/x-pack/test/api_integration/apis/synthetics/get_monitor_overview.ts @@ -55,7 +55,7 @@ export default function ({ getService }: FtrProviderContext) { }; before(async () => { - await supertest.post(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); + await supertest.put(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true').expect(200); await kibanaServer.spaces.create({ id: SPACE_ID, name: SPACE_NAME }); await security.role.create(roleName, { kibana: [ diff --git a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts index d031a6c505c8f..bf68da4c148f5 100644 --- a/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts +++ b/x-pack/test/api_integration/apis/synthetics/synthetics_enablement.ts @@ -6,24 +6,57 @@ */ import { API_URLS } from '@kbn/synthetics-plugin/common/constants'; +import { + syntheticsApiKeyID, + syntheticsApiKeyObjectType, +} from '@kbn/synthetics-plugin/server/legacy_uptime/lib/saved_objects/service_api_key'; import { serviceApiKeyPrivileges } from '@kbn/synthetics-plugin/server/synthetics_service/get_api_key'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { + const correctPrivileges = { + applications: [], + cluster: ['monitor', 'read_ilm', 'read_pipeline'], + indices: [ + { + allow_restricted_indices: false, + names: ['synthetics-*'], + privileges: ['view_index_metadata', 'create_doc', 'auto_configure', 'read'], + }, + ], + metadata: {}, + run_as: [], + transient_metadata: { + enabled: true, + }, + }; + describe('SyntheticsEnablement', () => { const supertestWithAuth = getService('supertest'); const supertest = getService('supertestWithoutAuth'); const security = getService('security'); const kibanaServer = getService('kibanaServer'); - before(async () => { - await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); - }); - - describe('[GET] - /internal/uptime/service/enablement', () => { - ['manage_security', 'manage_own_api_key', 'manage_api_key'].forEach((privilege) => { - it(`returns response for an admin with privilege ${privilege}`, async () => { + const esSupertest = getService('esSupertest'); + + const getApiKeys = async () => { + const { body } = await esSupertest.get(`/_security/api_key`).query({ with_limited_by: true }); + const apiKeys = body.api_keys || []; + return apiKeys.filter( + (apiKey: any) => apiKey.name.includes('synthetics-api-key') && apiKey.invalidated === false + ); + }; + + describe('[PUT] /internal/uptime/service/enablement', () => { + beforeEach(async () => { + const apiKeys = await getApiKeys(); + if (apiKeys.length) { + await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); + } + }); + ['manage_security', 'manage_api_key', 'manage_own_api_key'].forEach((privilege) => { + it(`returns response when user can manage api keys`, async () => { const username = 'admin'; const roleName = `synthetics_admin-${privilege}`; const password = `${username}-password`; @@ -38,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: [privilege, ...serviceApiKeyPrivileges.cluster], + cluster: [privilege], indices: serviceApiKeyPrivileges.indices, }, }); @@ -50,7 +83,7 @@ export default function ({ getService }: FtrProviderContext) { }); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -58,17 +91,10 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: true, - canEnable: true, - isEnabled: true, - isValidApiKey: true, + canEnable: false, + isEnabled: false, + isValidApiKey: false, }); - if (privilege !== 'manage_own_api_key') { - await supertest - .delete(API_URLS.SYNTHETICS_ENABLEMENT) - .auth(username, password) - .set('kbn-xsrf', 'true') - .expect(200); - } } finally { await security.user.delete(username); await security.role.delete(roleName); @@ -76,9 +102,9 @@ export default function ({ getService }: FtrProviderContext) { }); }); - it('returns response for an uptime all user without admin privileges', async () => { - const username = 'uptime'; - const roleName = 'uptime_user'; + it(`returns response for an admin with privilege`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; const password = `${username}-password`; try { await security.role.create(roleName, { @@ -90,7 +116,10 @@ export default function ({ getService }: FtrProviderContext) { spaces: ['*'], }, ], - elasticsearch: {}, + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, }); await security.user.create(username, { @@ -100,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) { }); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -108,19 +137,20 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: false, - canEnable: false, - isEnabled: false, - isValidApiKey: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, }); + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); } finally { - await security.role.delete(roleName); await security.user.delete(username); + await security.role.delete(roleName); } }); - }); - describe('[POST] - /internal/uptime/service/enablement', () => { - it('with an admin', async () => { + it(`does not create excess api keys`, async () => { const username = 'admin'; const roleName = `synthetics_admin`; const password = `${username}-password`; @@ -135,7 +165,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -146,38 +176,213 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); - await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + + // call api a second time + const apiResponse2 = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + + expect(apiResponse2.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + } finally { + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it(`auto re-enables the api key when created with invalid permissions and invalidates old api key`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + try { + // create api key with incorrect permissions + const apiKeyResult = await esSupertest + .post(`/_security/api_key`) + .send({ + name: 'synthetics-api-key', + expiration: '1d', + role_descriptors: { + 'role-a': { + cluster: serviceApiKeyPrivileges.cluster, + indices: [ + { + names: ['synthetics-*'], + privileges: ['view_index_metadata', 'create_doc', 'auto_configure'], + }, + ], + }, + }, + }) + .expect(200); + kibanaServer.savedObjects.create({ + id: syntheticsApiKeyID, + type: syntheticsApiKeyObjectType, + overwrite: true, + attributes: { + id: apiKeyResult.body.id, + name: 'synthetics-api-key (required for monitor management)', + apiKey: apiKeyResult.body.api_key, + }, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).not.eql(correctPrivileges); + + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); } finally { - await supertest - .delete(API_URLS.SYNTHETICS_ENABLEMENT) + await security.user.delete(username); + await security.role.delete(roleName); + } + }); + + it(`auto re-enables api key when invalidated`, async () => { + const username = 'admin'; + const roleName = `synthetics_admin`; + const password = `${username}-password`; + try { + await security.role.create(roleName, { + kibana: [ + { + feature: { + uptime: ['all'], + }, + spaces: ['*'], + }, + ], + elasticsearch: { + cluster: serviceApiKeyPrivileges.cluster, + indices: serviceApiKeyPrivileges.indices, + }, + }); + + await security.user.create(username, { + password, + roles: [roleName], + full_name: 'a kibana user', + }); + + const apiResponse = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) + .auth(username, password) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(apiResponse.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys = await getApiKeys(); + expect(validApiKeys.length).eql(1); + expect(validApiKeys[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + + // delete api key + await esSupertest + .delete(`/_security/api_key`) + .send({ + ids: [validApiKeys[0].id], + }) + .expect(200); + + const validApiKeysAferDeletion = await getApiKeys(); + expect(validApiKeysAferDeletion.length).eql(0); + + // call api a second time + const apiResponse2 = await supertest + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + + expect(apiResponse2.body).eql({ + areApiKeysEnabled: true, + canManageApiKeys: false, + canEnable: true, + isEnabled: true, + isValidApiKey: true, + }); + + const validApiKeys2 = await getApiKeys(); + expect(validApiKeys2.length).eql(1); + expect(validApiKeys2[0].role_descriptors.synthetics_writer).eql(correctPrivileges); + } finally { await security.user.delete(username); await security.role.delete(roleName); } }); - it('with an uptime user', async () => { + it('returns response for an uptime all user without admin privileges', async () => { const username = 'uptime'; - const roleName = `uptime_user`; + const roleName = 'uptime_user'; const password = `${username}-password`; try { await security.role.create(roleName, { @@ -198,16 +403,12 @@ export default function ({ getService }: FtrProviderContext) { full_name: 'a kibana user', }); - await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) - .auth(username, password) - .set('kbn-xsrf', 'true') - .expect(403); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); + expect(apiResponse.body).eql({ areApiKeysEnabled: true, canManageApiKeys: false, @@ -216,13 +417,19 @@ export default function ({ getService }: FtrProviderContext) { isValidApiKey: false, }); } finally { - await security.user.delete(username); await security.role.delete(roleName); + await security.user.delete(username); } }); }); - describe('[DELETE] - /internal/uptime/service/enablement', () => { + describe('[DELETE] /internal/uptime/service/enablement', () => { + beforeEach(async () => { + const apiKeys = await getApiKeys(); + if (apiKeys.length) { + await supertestWithAuth.delete(API_URLS.SYNTHETICS_ENABLEMENT).set('kbn-xsrf', 'true'); + } + }); it('with an admin', async () => { const username = 'admin'; const roleName = `synthetics_admin`; @@ -238,7 +445,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -250,7 +457,7 @@ export default function ({ getService }: FtrProviderContext) { }); await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -261,14 +468,14 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(delResponse.body).eql({}); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -303,7 +510,7 @@ export default function ({ getService }: FtrProviderContext) { }); await supertestWithAuth - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .set('kbn-xsrf', 'true') .expect(200); await supertest @@ -312,7 +519,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(403); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -351,7 +558,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], elasticsearch: { - cluster: ['manage_security', ...serviceApiKeyPrivileges.cluster], + cluster: serviceApiKeyPrivileges.cluster, indices: serviceApiKeyPrivileges.indices, }, }); @@ -364,21 +571,21 @@ export default function ({ getService }: FtrProviderContext) { // can enable synthetics in default space when enabled in a non default space const apiResponseGet = await supertest - .get(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponseGet.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, }); await supertest - .post(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) + .put(`/s/${SPACE_ID}${API_URLS.SYNTHETICS_ENABLEMENT}`) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -388,14 +595,14 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); const apiResponse = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -403,7 +610,7 @@ export default function ({ getService }: FtrProviderContext) { // can disable synthetics in non default space when enabled in default space await supertest - .post(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); @@ -413,14 +620,14 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'true') .expect(200); const apiResponse2 = await supertest - .get(API_URLS.SYNTHETICS_ENABLEMENT) + .put(API_URLS.SYNTHETICS_ENABLEMENT) .auth(username, password) .set('kbn-xsrf', 'true') .expect(200); expect(apiResponse2.body).eql({ areApiKeysEnabled: true, - canManageApiKeys: true, + canManageApiKeys: false, canEnable: true, isEnabled: true, isValidApiKey: true, @@ -428,6 +635,7 @@ export default function ({ getService }: FtrProviderContext) { } finally { await security.user.delete(username); await security.role.delete(roleName); + await kibanaServer.spaces.delete(SPACE_ID); } }); }); diff --git a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts index 7ec09849b7ff2..f95bb8de59a89 100644 --- a/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts +++ b/x-pack/test/apm_api_integration/tests/alerts/chart_preview.spec.ts @@ -83,6 +83,61 @@ export default function ApiTest({ getService }: FtrProviderContext) { ).to.equal(true); }); + it('transaction_error_rate with transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'APIRestController#product', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect(response.body.errorRateChartPreview[0]).to.eql({ + x: 1627974600000, + y: 1, + }); + }); + + it('transaction_error_rate with nonexistent transaction name', async () => { + const options = { + params: { + query: { + start, + end, + serviceName: 'opbeans-java', + transactionName: 'foo', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + interval: '5m', + }, + }, + }; + + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/rule_types/transaction_error_rate/chart_preview', + ...options, + }); + + expect(response.status).to.be(200); + expect( + response.body.errorRateChartPreview.every( + (item: { x: number; y: number | null }) => item.y === null + ) + ).to.equal(true); + }); + it('error_count (with data)', async () => { const options = getOptions(); options.params.query.transactionType = undefined; diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts new file mode 100644 index 0000000000000..635cf88965e5f --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/page_objects/csp_dashboard_page.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 type { FtrProviderContext } from '../ftr_provider_context'; + +// Defined in CSP plugin +const LATEST_FINDINGS_INDEX = 'logs-cloud_security_posture.findings_latest-default'; + +export function CspDashboardPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + const retry = getService('retry'); + const es = getService('es'); + const supertest = getService('supertest'); + const log = getService('log'); + + /** + * required before indexing findings + */ + const waitForPluginInitialized = (): Promise => + retry.try(async () => { + log.debug('Check CSP plugin is initialized'); + const response = await supertest + .get('/internal/cloud_security_posture/status?check=init') + .expect(200); + expect(response.body).to.eql({ isPluginInitialized: true }); + log.debug('CSP plugin is initialized'); + }); + + const index = { + remove: () => es.indices.delete({ index: LATEST_FINDINGS_INDEX, ignore_unavailable: true }), + add: async (findingsMock: T[]) => { + await Promise.all( + findingsMock.map((finding) => + es.index({ + index: LATEST_FINDINGS_INDEX, + body: finding, + }) + ) + ); + }, + }; + + const dashboard = { + getDashboardPageHeader: () => testSubjects.find('cloud-posture-dashboard-page-header'), + + getDashboardTabs: async () => { + const dashboardPageHeader = await dashboard.getDashboardPageHeader(); + return await dashboardPageHeader.findByClassName('euiTabs'); + }, + + getCloudTab: async () => { + const tabs = await dashboard.getDashboardTabs(); + return await tabs.findByXpath(`//span[text()="Cloud"]`); + }, + + getKubernetesTab: async () => { + const tabs = await dashboard.getDashboardTabs(); + return await tabs.findByXpath(`//span[text()="Kubernetes"]`); + }, + + clickTab: async (tab: 'Cloud' | 'Kubernetes') => { + if (tab === 'Cloud') { + const cloudTab = await dashboard.getCloudTab(); + await cloudTab.click(); + } + if (tab === 'Kubernetes') { + const k8sTab = await dashboard.getKubernetesTab(); + await k8sTab.click(); + } + }, + + getIntegrationDashboardContainer: () => testSubjects.find('dashboard-container'), + + // Cloud Dashboard + + getCloudDashboard: async () => { + await dashboard.clickTab('Cloud'); + return await testSubjects.find('cloud-dashboard-container'); + }, + + getCloudSummarySection: async () => { + await dashboard.getCloudDashboard(); + return await testSubjects.find('dashboard-summary-section'); + }, + + getCloudComplianceScore: async () => { + await dashboard.getCloudSummarySection(); + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + + // Kubernetes Dashboard + + getKubernetesDashboard: async () => { + await dashboard.clickTab('Kubernetes'); + return await testSubjects.find('kubernetes-dashboard-container'); + }, + + getKubernetesSummarySection: async () => { + await dashboard.getKubernetesDashboard(); + return await testSubjects.find('dashboard-summary-section'); + }, + + getKubernetesComplianceScore: async () => { + await dashboard.getKubernetesSummarySection(); + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + + getKubernetesComplianceScore2: async () => { + // await dashboard.getKubernetesSummarySection(); + return await testSubjects.find('dashboard-summary-section-compliance-score'); + }, + }; + + const navigateToComplianceDashboardPage = async () => { + await PageObjects.common.navigateToUrl( + 'securitySolution', // Defined in Security Solution plugin + 'cloud_security_posture/dashboard', + { shouldUseHashForSubUrl: false } + ); + }; + + return { + waitForPluginInitialized, + navigateToComplianceDashboardPage, + dashboard, + index, + }; +} diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts index e5738873edc51..26aacd8cca997 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/index.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/index.ts @@ -7,8 +7,10 @@ import { pageObjects as xpackFunctionalPageObjects } from '../../functional/page_objects'; import { FindingsPageProvider } from './findings_page'; +import { CspDashboardPageProvider } from './csp_dashboard_page'; export const pageObjects = { ...xpackFunctionalPageObjects, findings: FindingsPageProvider, + cloudPostureDashboard: CspDashboardPageProvider, }; diff --git a/x-pack/test/cloud_security_posture_functional/pages/compliance_dashboard.ts b/x-pack/test/cloud_security_posture_functional/pages/compliance_dashboard.ts new file mode 100644 index 0000000000000..cb4635899f5c3 --- /dev/null +++ b/x-pack/test/cloud_security_posture_functional/pages/compliance_dashboard.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 Chance from 'chance'; +import type { FtrProviderContext } from '../ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const retry = getService('retry'); + const pageObjects = getPageObjects(['common', 'cloudPostureDashboard']); + const chance = new Chance(); + + const data = [ + { + '@timestamp': new Date().toISOString(), + resource: { id: chance.guid(), name: `kubelet`, sub_type: 'lower case sub type' }, + result: { evaluation: 'failed' }, + rule: { + name: 'Upper case rule name', + section: 'Upper case section', + benchmark: { + id: 'cis_k8s', + posture_type: 'kspm', + }, + }, + cluster_id: 'Upper case cluster id', + }, + ]; + + describe('Cloud Posture Dashboard Page', () => { + let cspDashboard: typeof pageObjects.cloudPostureDashboard; + let dashboard: typeof pageObjects.cloudPostureDashboard.dashboard; + + before(async () => { + cspDashboard = pageObjects.cloudPostureDashboard; + dashboard = pageObjects.cloudPostureDashboard.dashboard; + await cspDashboard.waitForPluginInitialized(); + + await cspDashboard.index.add(data); + await cspDashboard.navigateToComplianceDashboardPage(); + await retry.waitFor( + 'Cloud posture integration dashboard to be displayed', + async () => !!dashboard.getIntegrationDashboardContainer() + ); + }); + + after(async () => { + await cspDashboard.index.remove(); + }); + + describe('Kubernetes Dashboard', () => { + it('displays accurate summary compliance score', async () => { + const scoreElement = await dashboard.getKubernetesComplianceScore(); + + expect((await scoreElement.getVisibleText()) === '0%').to.be(true); + }); + }); + }); +} diff --git a/x-pack/test/cloud_security_posture_functional/pages/index.ts b/x-pack/test/cloud_security_posture_functional/pages/index.ts index 80e96b8b17ce9..7566afda0501a 100644 --- a/x-pack/test/cloud_security_posture_functional/pages/index.ts +++ b/x-pack/test/cloud_security_posture_functional/pages/index.ts @@ -11,5 +11,6 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('Cloud Security Posture', function () { loadTestFile(require.resolve('./findings')); + loadTestFile(require.resolve('./compliance_dashboard')); }); } diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts index 62fa4d3786db6..2c6ae644d911c 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/create_rules.ts @@ -99,7 +99,6 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'query', threat: [], - throttle: 'no_actions', exceptions_list: [], version: 1, revision: 0, diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts index 4346087684fd4..e6adaa069b497 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules.ts @@ -7,7 +7,12 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; import { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ROLES } from '@kbn/security-solution-plugin/common/test'; @@ -32,8 +37,15 @@ import { waitForSignalsToBePresent, getThresholdRuleForSignalTesting, waitForRulePartialFailure, + createRule, } from '../../utils'; import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -200,7 +212,6 @@ export default ({ getService }: FtrProviderContext) => { to: 'now', type: 'query', threat: [], - throttle: 'no_actions', exceptions_list: [], version: 1, }; @@ -566,5 +577,139 @@ export default ({ getService }: FtrProviderContext) => { expect(rule?.execution_summary?.last_execution.status).to.eql('partial failure'); }); }); + + describe('per-action frequencies', () => { + const createSingleRule = async (rule: RuleCreateProps) => { + const createdRule = await createRule(supertest, log, rule); + createdRule.actions = removeUUIDFromActions(createdRule.actions); + return createdRule; + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = actionsWithFrequencies; + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRule(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await createSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutput(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + const rule = removeServerGeneratedProperties(createdRule); + expect(rule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts index 6d0e79975bfd6..63d7e18367dc6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/create_rules_bulk.ts @@ -10,7 +10,11 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_BULK_CREATE, DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, } from '@kbn/security-solution-plugin/common/constants'; +import { RuleCreateProps } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -27,6 +31,12 @@ import { removeServerGeneratedPropertiesIncludingRuleId, waitForRuleSuccess, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -276,6 +286,141 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + describe('per-action frequencies', () => { + const bulkCreateSingleRule = async (rule: RuleCreateProps) => { + const { body } = await supertest + .post(DETECTION_ENGINE_RULES_BULK_CREATE) + .set('kbn-xsrf', 'true') + .send([rule]) + .expect(200); + + const createdRule = body[0]; + createdRule.actions = removeUUIDFromActions(createdRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(createdRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(createdRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithoutFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = actionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = actionsWithFrequencies; + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(createdRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + const simpleRule = getSimpleRuleWithoutRuleId(); + simpleRule.throttle = throttle; + simpleRule.actions = someActionsWithFrequencies; + + const createdRule = await bulkCreateSingleRule(simpleRule); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(createdRule).to.eql(expectedRule); + }); + }); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts index a394bef16cd22..e029c13aff8e5 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules.ts @@ -176,6 +176,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts index fa83ceb9a19b1..8e0ad07a782c3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/delete_rules_bulk.ts @@ -309,6 +309,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -357,6 +358,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); expect(body[1].actions).to.eql([ @@ -368,6 +370,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts index c9cb430e144ba..72fb3631ac0c3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/export_rules.ts @@ -8,6 +8,7 @@ import expect from 'expect'; import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { RuleResponse } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { binaryToString, @@ -208,10 +209,17 @@ export default ({ getService }: FtrProviderContext): void => { const outputRule1: ReturnType = { ...getSimpleRuleOutput('rule-1'), actions: [ - { ...action1, uuid: firstRule.actions[0].uuid }, - { ...action2, uuid: firstRule.actions[1].uuid }, + { + ...action1, + uuid: firstRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + ...action2, + uuid: firstRule.actions[1].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, ], - throttle: 'rule', }; expect(firstRule).toEqual(outputRule1); }); @@ -258,13 +266,23 @@ export default ({ getService }: FtrProviderContext): void => { const outputRule1: ReturnType = { ...getSimpleRuleOutput('rule-2'), - actions: [{ ...action, uuid: firstRule.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: firstRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; const outputRule2: ReturnType = { ...getSimpleRuleOutput('rule-1'), - actions: [{ ...action, uuid: secondRule.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: secondRule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; expect(firstRule).toEqual(outputRule1); expect(secondRule).toEqual(outputRule2); @@ -437,9 +455,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const firstRule = removeServerGeneratedProperties(firstRuleParsed); @@ -514,6 +532,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -523,9 +542,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const firstRule = removeServerGeneratedProperties(firstRuleParsed); @@ -631,6 +650,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -640,9 +660,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const outputRule2: ReturnType = { @@ -656,6 +676,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { group: 'default', @@ -665,9 +686,9 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; const firstRuleParsed = JSON.parse(body.toString().split(/\n/)[0]); const secondRuleParsed = JSON.parse(body.toString().split(/\n/)[1]); @@ -682,7 +703,8 @@ export default ({ getService }: FtrProviderContext): void => { }); }; -function expectToMatchRuleSchema(obj: unknown): void { +function expectToMatchRuleSchema(obj: RuleResponse): void { + expect(obj.throttle).toBeUndefined(); expect(obj).toEqual({ id: expect.any(String), rule_id: expect.any(String), @@ -718,7 +740,6 @@ function expectToMatchRuleSchema(obj: unknown): void { language: expect.any(String), index: expect.arrayContaining([]), query: expect.any(String), - throttle: expect.any(String), actions: expect.arrayContaining([]), }); } diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts index 7cd260697be72..348400580edd2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group1/find_rules.ts @@ -126,8 +126,13 @@ export default ({ getService }: FtrProviderContext): void => { const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: body.data[0].actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: body.data[0].actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; body.data = [removeServerGeneratedProperties(body.data[0])]; @@ -171,8 +176,13 @@ export default ({ getService }: FtrProviderContext): void => { const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: body.data[0].actions[0].uuid }], - throttle: '1h', // <-- throttle makes this a scheduled action + actions: [ + { + ...action, + uuid: body.data[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + ], }; body.data = [removeServerGeneratedProperties(body.data[0])]; @@ -239,9 +249,9 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: hookAction.actionTypeId, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; body.data = [removeServerGeneratedProperties(body.data[0])]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts index 6ba7871b1dbf5..9fd2542664f3b 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/legacy_actions_migrations.ts @@ -75,8 +75,8 @@ export default ({ getService }: FtrProviderContext) => { expect(sidecarActionsSOAfterMigration.hits.hits.length).to.eql(0); expect(ruleSO?.alert.actions).to.eql([]); - expect(ruleSO?.alert.throttle).to.eql('no_actions'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); }); it('migrates legacy actions for rule with action run on every run', async () => { @@ -122,6 +122,7 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, { actionRef: 'action_1', @@ -133,10 +134,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('rule'); - expect(ruleSO?.alert.notifyWhen).to.eql('onActiveAlert'); + expect(ruleSO?.alert.throttle).to.eql(null); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -197,6 +199,7 @@ export default ({ getService }: FtrProviderContext) => { actionRef: 'action_0', group: 'default', uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, { actionTypeId: '.slack', @@ -206,10 +209,11 @@ export default ({ getService }: FtrProviderContext) => { actionRef: 'action_1', group: 'default', uuid: ruleSO?.alert.actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('1h'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -269,10 +273,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('1d'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', @@ -327,10 +332,11 @@ export default ({ getService }: FtrProviderContext) => { to: ['test@test.com'], }, uuid: ruleSO?.alert.actions[0].uuid, + frequency: { summary: true, throttle: '7d', notifyWhen: 'onThrottleInterval' }, }, ]); - expect(ruleSO?.alert.throttle).to.eql('7d'); - expect(ruleSO?.alert.notifyWhen).to.eql('onThrottleInterval'); + expect(ruleSO?.alert.throttle).to.eql(undefined); + expect(ruleSO?.alert.notifyWhen).to.eql(null); expect(ruleSO?.references).to.eql([ { id: 'c95cb100-b075-11ec-bb3f-1f063f8e06cf', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts index 98b64726bbaca..ae434a50df98f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules.ts @@ -7,7 +7,13 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -24,7 +30,14 @@ import { getSimpleMlRule, createLegacyRuleAction, getLegacyActionSO, + getSimpleRuleWithoutRuleId, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -377,9 +390,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; - outputRule.throttle = '1h'; outputRule.revision = 1; expect(bodyToCompare).to.eql(outputRule); @@ -414,5 +427,168 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); + + describe('patch per-action frequencies', () => { + const patchSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + const { body: patchedRule } = await supertest + .patch(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send({ rule_id: ruleId, throttle, actions }) + .expect(200); + + patchedRule.actions = removeUUIDFromActions(patchedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(patchedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // patch a simple rule's `throttle` and `actions` + const patchedRule = await patchSingleRule( + createdRule.rule_id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(patchedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts index 1a26b17bca42e..2d80d17f9100e 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/patch_rules_bulk.ts @@ -207,9 +207,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; - outputRule.throttle = '1h'; outputRule.revision = 1; expect(bodyToCompare).to.eql(outputRule); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts index c77ac5f142f92..185d820351459 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/perform_bulk_action.ts @@ -9,7 +9,6 @@ import expect from '@kbn/expect'; import { DETECTION_ENGINE_RULES_BULK_ACTION, DETECTION_ENGINE_RULES_URL, - NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE, } from '@kbn/security-solution-plugin/common/constants'; import type { RuleResponse } from '@kbn/security-solution-plugin/common/detection_engine/rule_schema'; @@ -17,7 +16,10 @@ import { BulkActionType, BulkActionEditType, } from '@kbn/security-solution-plugin/common/detection_engine/rule_management/api/rules/bulk_actions/request_schema'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL } from '@kbn/securitysolution-list-constants'; +import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; +import { deleteAllExceptions } from '../../../lists_api_integration/utils'; import { binaryToString, createLegacyRuleAction, @@ -35,6 +37,7 @@ import { removeServerGeneratedProperties, waitForRuleSuccess, } from '../../utils'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -168,7 +171,6 @@ export default ({ getService }: FtrProviderContext): void => { const rule = removeServerGeneratedProperties(JSON.parse(ruleJson)); expect(rule).to.eql({ ...getSimpleRuleOutput(), - throttle: 'rule', actions: [ { action_type_id: '.webhook', @@ -178,6 +180,7 @@ export default ({ getService }: FtrProviderContext): void => { body: '{"test":"a default action"}', }, uuid: rule.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ], }); @@ -331,6 +334,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: ruleBody.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); // we want to ensure rule is executing successfully, to prevent any AAD issues related to partial update of rule SO @@ -403,6 +407,7 @@ export default ({ getService }: FtrProviderContext): void => { message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: ruleBody.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -416,15 +421,225 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.duplicate, - duplicate: { include_exceptions: false }, + duplicate: { include_exceptions: false, include_expired_exceptions: false }, + }) + .expect(200); + + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + + // Check that the duplicated rule is returned with the response + expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + + // Check that the updates have been persisted + const { body: rulesResponse } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(rulesResponse.total).to.eql(2); + }); + + it('should duplicate rules with exceptions - expired exceptions included', async () => { + await deleteAllExceptions(supertest, log); + + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + const { body: exceptionList } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + // create an exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate }) + .expect(200); + + const ruleId = 'ruleId'; + const ruleToDuplicate = { + ...getSimpleRule(ruleId), + exceptions_list: [ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + ], + }; + const newRule = await createRule(supertest, log, ruleToDuplicate); + + // add an exception item to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${newRule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .send({ + items: [ + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: 'Sample exception item', + type: 'simple', + expire_time: expiredDate, + }, + ], + }) + .expect(200); + + const { body } = await postBulkAction() + .send({ + query: '', + action: BulkActionType.duplicate, + duplicate: { include_exceptions: true, include_expired_exceptions: true }, + }) + .expect(200); + + const { body: foundItems } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${body.attributes.results.created[0].exceptions_list[1].list_id}` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Item should have been duplicated, even if expired + expect(foundItems.total).to.eql(1); + + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); + + // Check that the duplicated rule is returned with the response + expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + + // Check that the exceptions are duplicated + expect(body.attributes.results.created[0].exceptions_list).to.eql([ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + { + id: body.attributes.results.created[0].exceptions_list[1].id, + list_id: body.attributes.results.created[0].exceptions_list[1].list_id, + namespace_type: 'single', + type: 'rule_default', + }, + ]); + + // Check that the updates have been persisted + const { body: rulesResponse } = await supertest + .get(`${DETECTION_ENGINE_RULES_URL}/_find`) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(rulesResponse.total).to.eql(2); + }); + + it('should duplicate rules with exceptions - expired exceptions excluded', async () => { + await deleteAllExceptions(supertest, log); + + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + const { body: exceptionList } = await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + // create an exception list item + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate }) + .expect(200); + + const ruleId = 'ruleId'; + const ruleToDuplicate = { + ...getSimpleRule(ruleId), + exceptions_list: [ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + ], + }; + const newRule = await createRule(supertest, log, ruleToDuplicate); + + // add an exception item to the rule + await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/${newRule.id}/exceptions`) + .set('kbn-xsrf', 'true') + .send({ + items: [ + { + description: 'Exception item for rule default exception list', + entries: [ + { + field: 'some.not.nested.field', + operator: 'included', + type: 'match', + value: 'some value', + }, + ], + name: 'Sample exception item', + type: 'simple', + expire_time: expiredDate, + }, + ], + }) + .expect(200); + + const { body } = await postBulkAction() + .send({ + query: '', + action: BulkActionType.duplicate, + duplicate: { include_exceptions: true, include_expired_exceptions: false }, }) .expect(200); + const { body: foundItems } = await supertest + .get( + `${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${body.attributes.results.created[0].exceptions_list[1].list_id}` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + // Item should NOT have been duplicated, since it is expired + expect(foundItems.total).to.eql(0); + expect(body.attributes.summary).to.eql({ failed: 0, skipped: 0, succeeded: 1, total: 1 }); // Check that the duplicated rule is returned with the response expect(body.attributes.results.created[0].name).to.eql(`${ruleToDuplicate.name} [Duplicate]`); + // Check that the exceptions are duplicted + expect(body.attributes.results.created[0].exceptions_list).to.eql([ + { + type: exceptionList.type, + list_id: exceptionList.list_id, + id: exceptionList.id, + namespace_type: exceptionList.namespace_type, + }, + { + id: body.attributes.results.created[0].exceptions_list[1].id, + list_id: body.attributes.results.created[0].exceptions_list[1].list_id, + namespace_type: 'single', + type: 'rule_default', + }, + ]); + // Check that the updates have been persisted const { body: rulesResponse } = await supertest .get(`${DETECTION_ENGINE_RULES_URL}/_find`) @@ -462,7 +677,7 @@ export default ({ getService }: FtrProviderContext): void => { .send({ query: '', action: BulkActionType.duplicate, - duplicate: { include_exceptions: false }, + duplicate: { include_exceptions: false, include_expired_exceptions: false }, }) .expect(200); @@ -491,6 +706,7 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, ...(uuid ? { uuid } : {}), + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -1120,6 +1336,7 @@ export default ({ getService }: FtrProviderContext): void => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: setTagsRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); }); @@ -1404,6 +1621,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1461,6 +1679,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1572,6 +1791,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1624,6 +1844,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1678,12 +1899,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const expectedRuleActions = [ - { ...defaultRuleAction, uuid: body.attributes.results.updated[0].actions[0].uuid }, + { + ...defaultRuleAction, + uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, { ...webHookActionMock, id: webHookConnector.id, action_type_id: '.webhook', uuid: body.attributes.results.updated[0].actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1746,12 +1972,17 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); const expectedRuleActions = [ - { ...defaultRuleAction, uuid: body.attributes.results.updated[0].actions[0].uuid }, + { + ...defaultRuleAction, + uuid: body.attributes.results.updated[0].actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, { ...slackConnectorMockProps, id: slackConnector.id, action_type_id: '.slack', uuid: body.attributes.results.updated[0].actions[1].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]; @@ -1800,20 +2031,22 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].actions).to.eql([ - { ...defaultRuleAction, uuid: createdRule.actions[0].uuid }, - ]); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); expect(readRule.actions).to.eql([ - { ...defaultRuleAction, uuid: createdRule.actions[0].uuid }, + { + ...defaultRuleAction, + uuid: createdRule.actions[0].uuid, + frequency: { summary: true, throttle: '1d', notifyWhen: 'onThrottleInterval' }, + }, ]); }); - it('should change throttle if actions list in payload is empty', async () => { + it('should not change throttle if actions list in payload is empty', async () => { // create a new connector const webHookConnector = await createWebHookConnector(); @@ -1849,13 +2082,14 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].throttle).to.be('1h'); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: readRule } = await fetchRule(ruleId).expect(200); - expect(readRule.throttle).to.eql('1h'); + expect(readRule.throttle).to.eql(undefined); + expect(readRule.actions).to.eql(createdRule.actions); }); }); @@ -1903,6 +2137,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: editedRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); // version of prebuilt rule should not change @@ -1917,6 +2152,7 @@ export default ({ getService }: FtrProviderContext): void => { id: webHookConnector.id, action_type_id: '.webhook', uuid: readRule.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ]); expect(prebuiltRule.version).to.be(readRule.version); @@ -1993,7 +2229,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ]; casesForEmptyActions.forEach(({ payloadThrottle }) => { - it(`throttle is set to NOTIFICATION_THROTTLE_NO_ACTIONS, if payload throttle="${payloadThrottle}" and actions list is empty`, async () => { + it(`should not update throttle, if payload throttle="${payloadThrottle}" and actions list is empty`, async () => { const ruleId = 'ruleId'; const createdRule = await createRule(supertest, log, { ...getSimpleRule(ruleId), @@ -2016,34 +2252,32 @@ export default ({ getService }: FtrProviderContext): void => { }) .expect(200); - // Check that the updated rule is returned with the response - expect(body.attributes.results.updated[0].throttle).to.eql( - NOTIFICATION_THROTTLE_NO_ACTIONS - ); + // Check that the rule is skipped and was not updated + expect(body.attributes.results.skipped[0].id).to.eql(createdRule.id); // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); - expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(rule.throttle).to.eql(undefined); }); }); const casesForNonEmptyActions = [ { payloadThrottle: NOTIFICATION_THROTTLE_RULE, - expectedThrottle: NOTIFICATION_THROTTLE_RULE, + expectedThrottle: undefined, }, { payloadThrottle: '1h', - expectedThrottle: '1h', + expectedThrottle: undefined, }, { payloadThrottle: '1d', - expectedThrottle: '1d', + expectedThrottle: undefined, }, { payloadThrottle: '7d', - expectedThrottle: '7d', + expectedThrottle: undefined, }, ]; [BulkActionEditType.set_rule_actions, BulkActionEditType.add_rule_actions].forEach( @@ -2081,10 +2315,23 @@ export default ({ getService }: FtrProviderContext): void => { // Check that the updated rule is returned with the response expect(body.attributes.results.updated[0].throttle).to.eql(expectedThrottle); + const expectedActions = body.attributes.results.updated[0].actions.map( + (action: any) => ({ + ...action, + frequency: { + summary: true, + throttle: payloadThrottle !== 'rule' ? payloadThrottle : null, + notifyWhen: + payloadThrottle !== 'rule' ? 'onThrottleInterval' : 'onActiveAlert', + }, + }) + ); + // Check that the updates have been persisted const { body: rule } = await fetchRule(ruleId).expect(200); expect(rule.throttle).to.eql(expectedThrottle); + expect(rule.actions).to.eql(expectedActions); }); }); } @@ -2097,11 +2344,11 @@ export default ({ getService }: FtrProviderContext): void => { const cases = [ { payload: { throttle: '1d' }, - expected: { notifyWhen: 'onThrottleInterval' }, + expected: { notifyWhen: null }, }, { payload: { throttle: NOTIFICATION_THROTTLE_RULE }, - expected: { notifyWhen: 'onActiveAlert' }, + expected: { notifyWhen: null }, }, ]; cases.forEach(({ payload, expected }) => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts index d2162e02d443e..31904342e294f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/read_rules.ts @@ -135,8 +135,13 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(body); const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: bodyToCompare.actions[0].uuid }], - throttle: 'rule', + actions: [ + { + ...action, + uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + ], }; expect(bodyToCompare).to.eql(ruleWithActions); }); @@ -174,8 +179,13 @@ export default ({ getService }: FtrProviderContext) => { const bodyToCompare = removeServerGeneratedProperties(body); const ruleWithActions: ReturnType = { ...getSimpleRuleOutput(), - actions: [{ ...action, uuid: bodyToCompare.actions[0].uuid }], - throttle: '1h', // <-- throttle makes this a scheduled action + actions: [ + { + ...action, + uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, + }, + ], }; expect(bodyToCompare).to.eql(ruleWithActions); }); @@ -236,9 +246,9 @@ export default ({ getService }: FtrProviderContext) => { 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', }, action_type_id: hookAction.actionTypeId, + frequency: { summary: true, throttle: '1h', notifyWhen: 'onThrottleInterval' }, }, ], - throttle: '1h', }; expect(bodyToCompare).to.eql(ruleWithActions); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts index eedfaee3ec531..4b218d07b7613 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/throttle.ts @@ -56,7 +56,7 @@ export default ({ getService }: FtrProviderContext) => { }); describe('creating a rule', () => { - it('When creating a new action and attaching it to a rule, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating a new action and attaching it to a rule, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -66,10 +66,16 @@ export default ({ getService }: FtrProviderContext) => { const rule = await createRule(supertest, log, getRuleWithWebHookAction(hookAction.id)); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: null, + notify_when: 'onActiveAlert', + }); + expect(notifyWhen).to.eql(null); }); it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { @@ -105,7 +111,7 @@ export default ({ getService }: FtrProviderContext) => { expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { const ruleWithThrottle: RuleCreateProps = { ...getSimpleRule(), throttle: NOTIFICATION_THROTTLE_RULE, @@ -115,7 +121,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -125,10 +131,10 @@ export default ({ getService }: FtrProviderContext) => { throttle: NOTIFICATION_THROTTLE_RULE, }; const rule = await createRule(supertest, log, ruleWithThrottle); - expect(rule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(rule.throttle).to.eql(undefined); }); - it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onActiveAlert"', async () => { + it('When creating throttle with "NOTIFICATION_THROTTLE_RULE" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -142,13 +148,19 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: null, + notify_when: 'onActiveAlert', + }); + expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "1h" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => { + it('When creating throttle with "1h" set and no actions, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { const ruleWithThrottle: RuleCreateProps = { ...getSimpleRule(), throttle: '1h', @@ -158,10 +170,10 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onThrottleInterval'); + expect(notifyWhen).to.eql(null); }); - it('When creating throttle with "1h" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to "onThrottleInterval"', async () => { + it('When creating throttle with "1h" set and actions set, the rule should have its kibana alerting "mute_all" set to "false" and notify_when set to null', async () => { // create a new action const { body: hookAction } = await supertest .post('/api/actions/action') @@ -175,10 +187,16 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const { - body: { mute_all: muteAll, notify_when: notifyWhen }, + body: { mute_all: muteAll, notify_when: notifyWhen, actions }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onThrottleInterval'); + expect(actions.length).to.eql(1); + expect(actions[0].frequency).to.eql({ + summary: true, + throttle: '1h', + notify_when: 'onThrottleInterval', + }); + expect(notifyWhen).to.eql(null); }); }); @@ -193,7 +211,7 @@ export default ({ getService }: FtrProviderContext) => { const rule = await createRule(supertest, log, getRuleWithWebHookAction(hookAction.id)); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(readRule.throttle).to.eql(undefined); }); it('When creating throttle with "NOTIFICATION_THROTTLE_NO_ACTIONS" set and no actions, we should return "NOTIFICATION_THROTTLE_NO_ACTIONS" when doing a read', async () => { @@ -203,7 +221,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -214,7 +232,7 @@ export default ({ getService }: FtrProviderContext) => { }; const rule = await createRule(supertest, log, ruleWithThrottle); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); it('When creating a new action and attaching it to a rule, if we change the alert to a "muteAll" through the kibana alerting API, we should get back "NOTIFICATION_THROTTLE_NO_ACTIONS" ', async () => { @@ -232,7 +250,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(204); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); }); @@ -249,7 +267,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, ruleWithWebHookAction); ruleWithWebHookAction.name = 'some other name'; const updated = await updateRule(supertest, log, ruleWithWebHookAction); - expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(updated.throttle).to.eql(undefined); }); it('will not change the "muteAll" or "notifyWhen" if we update some part of the rule', async () => { @@ -268,7 +286,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${updated.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -284,7 +302,7 @@ export default ({ getService }: FtrProviderContext) => { await createRule(supertest, log, ruleWithWebHookAction); ruleWithWebHookAction.actions = []; const updated = await updateRule(supertest, log, ruleWithWebHookAction); - expect(updated.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(updated.throttle).to.eql(undefined); }); }); @@ -306,7 +324,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ rule_id: rule.rule_id, name: 'some other name' }) .expect(200); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_RULE); + expect(readRule.throttle).to.eql(undefined); }); it('will not change the "muteAll" or "notifyWhen" if we patch part of the rule', async () => { @@ -329,7 +347,7 @@ export default ({ getService }: FtrProviderContext) => { body: { mute_all: muteAll, notify_when: notifyWhen }, } = await supertest.get(`/api/alerting/rule/${rule.id}`); expect(muteAll).to.eql(false); - expect(notifyWhen).to.eql('onActiveAlert'); + expect(notifyWhen).to.eql(null); }); // NOTE: This shows A side effect of how we do not set data on side cars anymore where the user is told they have no actions since the array is empty. @@ -350,7 +368,7 @@ export default ({ getService }: FtrProviderContext) => { .send({ rule_id: rule.rule_id, actions: [] }) .expect(200); const readRule = await getRule(supertest, log, rule.rule_id); - expect(readRule.throttle).to.eql(NOTIFICATION_THROTTLE_NO_ACTIONS); + expect(readRule.throttle).to.eql(undefined); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts index 8396f72a9bb08..82b23e8b381d1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules.ts @@ -7,7 +7,13 @@ import expect from '@kbn/expect'; -import { DETECTION_ENGINE_RULES_URL } from '@kbn/security-solution-plugin/common/constants'; +import { + DETECTION_ENGINE_RULES_URL, + NOTIFICATION_DEFAULT_FREQUENCY, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, +} from '@kbn/security-solution-plugin/common/constants'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { @@ -28,7 +34,14 @@ import { createLegacyRuleAction, getThresholdRuleForSignalTesting, getLegacyActionSO, + getSimpleRuleWithoutRuleId, } from '../../utils'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -185,8 +198,6 @@ export default ({ getService }: FtrProviderContext) => { outputRule.revision = 1; // Expect an empty array outputRule.actions = []; - // Expect "no_actions" - outputRule.throttle = 'no_actions'; const bodyToCompare = removeServerGeneratedPropertiesIncludingRuleId(body); expect(bodyToCompare).to.eql(outputRule); }); @@ -221,7 +232,7 @@ export default ({ getService }: FtrProviderContext) => { id: connector.body.id, action_type_id: connector.body.connector_type_id, params: { - message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, }; // update a simple rule's name @@ -229,7 +240,6 @@ export default ({ getService }: FtrProviderContext) => { updatedRule.rule_id = createRuleBody.rule_id; updatedRule.name = 'some other name'; updatedRule.actions = [action1]; - updatedRule.throttle = '1d'; delete updatedRule.id; const { body } = await supertest @@ -249,13 +259,12 @@ export default ({ getService }: FtrProviderContext) => { group: 'default', id: connector.body.id, params: { - message: - 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions![0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]; - outputRule.throttle = '1d'; expect(bodyToCompare).to.eql(outputRule); @@ -662,6 +671,176 @@ export default ({ getService }: FtrProviderContext) => { expect(outputRule.saved_id).to.be(undefined); }); }); + + describe('per-action frequencies', () => { + const updateSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + // update a simple rule's `throttle` and `actions` + const ruleToUpdate = getSimpleRuleUpdate(); + ruleToUpdate.throttle = throttle; + ruleToUpdate.actions = actions; + ruleToUpdate.id = ruleId; + delete ruleToUpdate.rule_id; + + const { body: updatedRule } = await supertest + .put(DETECTION_ENGINE_RULES_URL) + .set('kbn-xsrf', 'true') + .send(ruleToUpdate) + .expect(200); + + updatedRule.actions = removeUUIDFromActions(updatedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(updatedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await updateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts index 0c8dcacd0ebbf..5da2e8f1c10cd 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/group10/update_rules_bulk.ts @@ -11,8 +11,12 @@ import { RuleResponse } from '@kbn/security-solution-plugin/common/detection_eng import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_RULES_BULK_UPDATE, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + NOTIFICATION_DEFAULT_FREQUENCY, } from '@kbn/security-solution-plugin/common/constants'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; +import { RuleActionArray, RuleActionThrottle } from '@kbn/securitysolution-io-ts-alerting-types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -25,7 +29,16 @@ import { getSimpleRule, createLegacyRuleAction, getLegacyActionSO, + removeServerGeneratedPropertiesIncludingRuleId, + getSimpleRuleWithoutRuleId, + getSimpleRuleOutputWithoutRuleId, } from '../../utils'; +import { removeUUIDFromActions } from '../../utils/remove_uuid_from_actions'; +import { + getActionsWithFrequencies, + getActionsWithoutFrequencies, + getSomeActionsWithFrequencies, +} from '../../utils/get_rule_actions'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext) => { @@ -138,7 +151,7 @@ export default ({ getService }: FtrProviderContext) => { id: connector.body.id, action_type_id: connector.body.connector_type_id, params: { - message: 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, }; const [rule1, rule2] = await Promise.all([ @@ -160,12 +173,10 @@ export default ({ getService }: FtrProviderContext) => { const updatedRule1 = getSimpleRuleUpdate('rule-1'); updatedRule1.name = 'some other name'; updatedRule1.actions = [action1]; - updatedRule1.throttle = '1d'; const updatedRule2 = getSimpleRuleUpdate('rule-2'); updatedRule2.name = 'some other name'; updatedRule2.actions = [action1]; - updatedRule2.throttle = '1d'; // update both rule names const { body }: { body: RuleResponse[] } = await supertest @@ -189,13 +200,12 @@ export default ({ getService }: FtrProviderContext) => { group: 'default', id: connector.body.id, params: { - message: - 'Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts', + message: 'Rule {{context.rule.name}} generated {{state.signals_count}} alerts', }, uuid: bodyToCompare.actions[0].uuid, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, }, ]; - outputRule.throttle = '1d'; expect(bodyToCompare).to.eql(outputRule); }); @@ -247,7 +257,6 @@ export default ({ getService }: FtrProviderContext) => { outputRule.name = 'some other name'; outputRule.revision = 1; outputRule.actions = []; - outputRule.throttle = 'no_actions'; const bodyToCompare = removeServerGeneratedProperties(response); expect(bodyToCompare).to.eql(outputRule); }); @@ -603,5 +612,177 @@ export default ({ getService }: FtrProviderContext) => { ]); }); }); + + describe('bulk per-action frequencies', () => { + const bulkUpdateSingleRule = async ( + ruleId: string, + throttle: RuleActionThrottle | undefined, + actions: RuleActionArray + ) => { + // update a simple rule's `throttle` and `actions` + const ruleToUpdate = getSimpleRuleUpdate(); + ruleToUpdate.throttle = throttle; + ruleToUpdate.actions = actions; + ruleToUpdate.id = ruleId; + delete ruleToUpdate.rule_id; + + const { body } = await supertest + .put(DETECTION_ENGINE_RULES_BULK_UPDATE) + .set('kbn-xsrf', 'true') + .send([ruleToUpdate]) + .expect(200); + + const updatedRule = body[0]; + updatedRule.actions = removeUUIDFromActions(updatedRule.actions); + return removeServerGeneratedPropertiesIncludingRuleId(updatedRule); + }; + + describe('actions without frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it sets each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['300s', '5m', '3h', '4d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and sets it as a frequency of each action`, async () => { + const actionsWithoutFrequencies = await getActionsWithoutFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithoutFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithoutFrequencies.map((action) => ({ + ...action, + frequency: { summary: true, throttle, notifyWhen: 'onThrottleInterval' }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('actions with frequencies', () => { + [ + undefined, + NOTIFICATION_THROTTLE_NO_ACTIONS, + NOTIFICATION_THROTTLE_RULE, + '321s', + '6m', + '10h', + '2d', + ].forEach((throttle) => { + it(`it does not change actions frequency attributes when 'throttle' is '${throttle}'`, async () => { + const actionsWithFrequencies = await getActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + actionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = actionsWithFrequencies; + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + + describe('some actions with frequencies', () => { + [undefined, NOTIFICATION_THROTTLE_NO_ACTIONS, NOTIFICATION_THROTTLE_RULE].forEach( + (throttle) => { + it(`it overrides each action's frequency attribute to default value when 'throttle' is ${throttle}`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? NOTIFICATION_DEFAULT_FREQUENCY, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + } + ); + + // Action throttle cannot be shorter than the schedule interval which is by default is 5m + ['430s', '7m', '1h', '8d'].forEach((throttle) => { + it(`it correctly transforms 'throttle = ${throttle}' and overrides frequency attribute of each action`, async () => { + const someActionsWithFrequencies = await getSomeActionsWithFrequencies(supertest); + + // create simple rule + const createdRule = await createRule(supertest, log, getSimpleRuleWithoutRuleId()); + + // update a simple rule's `throttle` and `actions` + const updatedRule = await bulkUpdateSingleRule( + createdRule.id, + throttle, + someActionsWithFrequencies + ); + + const expectedRule = getSimpleRuleOutputWithoutRuleId(); + expectedRule.revision = 1; + expectedRule.actions = someActionsWithFrequencies.map((action) => ({ + ...action, + frequency: action.frequency ?? { + summary: true, + throttle, + notifyWhen: 'onThrottleInterval', + }, + })); + + expect(updatedRule).to.eql(expectedRule); + }); + }); + }); + }); }); }; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts index af66123014383..31b8c4571e2f5 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_complex_rule_output.ts @@ -92,7 +92,6 @@ export const getComplexRuleOutput = (ruleId = 'rule-1'): Partial = 'http://www.example.com/some-article-about-attack', 'Some plain text string here explaining why this is a valid thing to look out for', ], - throttle: 'no_actions', timeline_id: 'timeline_id', timeline_title: 'timeline_title', updated_by: 'elastic', diff --git a/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts b/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts new file mode 100644 index 0000000000000..519cd9136c604 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/get_rule_actions.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 SuperTest from 'supertest'; + +import { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; +import { getSlackAction } from './get_slack_action'; +import { getWebHookAction } from './get_web_hook_action'; + +const createConnector = async ( + supertest: SuperTest.SuperTest, + payload: Record +) => + (await supertest.post('/api/actions/action').set('kbn-xsrf', 'true').send(payload).expect(200)) + .body; +const createWebHookConnector = (supertest: SuperTest.SuperTest) => + createConnector(supertest, getWebHookAction()); +const createSlackConnector = (supertest: SuperTest.SuperTest) => + createConnector(supertest, getSlackAction()); + +export const getActionsWithoutFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + }, + ]; +}; + +export const getActionsWithFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + frequency: { summary: false, throttle: '3d', notifyWhen: 'onThrottleInterval' }, + }, + ]; +}; + +export const getSomeActionsWithFrequencies = async ( + supertest: SuperTest.SuperTest +): Promise => { + const webHookAction = await createWebHookConnector(supertest); + const slackConnector = await createSlackConnector(supertest); + return [ + { + group: 'default', + id: webHookAction.id, + action_type_id: '.webhook', + params: { message: 'Email message' }, + frequency: { summary: true, throttle: null, notifyWhen: 'onActiveAlert' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + frequency: { summary: false, throttle: '3d', notifyWhen: 'onThrottleInterval' }, + }, + { + group: 'default', + id: slackConnector.id, + action_type_id: '.slack', + params: { message: 'Slack message' }, + }, + ]; +}; diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts index 9826d9a2b98b4..cdadcce39e0a8 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output.ts @@ -40,7 +40,7 @@ export const getMockSharedResponseSchema = ( tags: [], to: 'now', threat: [], - throttle: 'no_actions', + throttle: undefined, exceptions_list: [], version: 1, revision: 0, diff --git a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts index 25776fefd5687..7ecee679e50b3 100644 --- a/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts +++ b/x-pack/test/detection_engine_api_integration/utils/get_simple_rule_output_with_web_hook_action.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { NOTIFICATION_DEFAULT_FREQUENCY } from '@kbn/security-solution-plugin/common/constants'; import { getSimpleRuleOutput } from './get_simple_rule_output'; import { RuleWithoutServerGeneratedProperties } from './remove_server_generated_properties'; @@ -13,7 +14,6 @@ export const getSimpleRuleOutputWithWebHookAction = ( uuid: string ): RuleWithoutServerGeneratedProperties => ({ ...getSimpleRuleOutput(), - throttle: 'rule', actions: [ { action_type_id: '.webhook', @@ -23,6 +23,7 @@ export const getSimpleRuleOutputWithWebHookAction = ( body: '{}', }, uuid, + frequency: NOTIFICATION_DEFAULT_FREQUENCY, }, ], }); diff --git a/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts b/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts new file mode 100644 index 0000000000000..08d95bc750212 --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/utils/remove_uuid_from_actions.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RuleActionArray } from '@kbn/securitysolution-io-ts-alerting-types'; + +export const removeUUIDFromActions = (actions: RuleActionArray): RuleActionArray => { + return actions.map(({ uuid, ...restOfAction }) => ({ + ...restOfAction, + })); +}; diff --git a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts index 92320dad62087..7731eea1d9d7a 100644 --- a/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/functional/apps/aiops/explain_log_rate_spikes.ts @@ -13,8 +13,8 @@ import type { FtrProviderContext } from '../../ftr_provider_context'; import { isTestDataExpectedWithSampleProbability, type TestData } from './types'; import { explainLogRateSpikesTestData } from './test_data'; -export default function ({ getPageObject, getService }: FtrProviderContext) { - const headerPage = getPageObject('header'); +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'console', 'header', 'home', 'security']); const elasticChart = getService('elasticChart'); const aiops = getService('aiops'); @@ -58,7 +58,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.assertSamplingProbabilityMissing(); } - await headerPage.waitUntilLoadingHasFinished(); + await PageObjects.header.waitUntilLoadingHasFinished(); await ml.testExecution.logTestStep( `${testData.suiteTitle} displays elements in the doc count panel correctly` @@ -78,77 +78,78 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await aiops.explainLogRateSpikesPage.clickDocumentCountChart(testData.chartClickCoordinates); await aiops.explainLogRateSpikesPage.assertAnalysisSectionExists(); - await ml.testExecution.logTestStep('displays the no results found prompt'); - await aiops.explainLogRateSpikesPage.assertNoResultsFoundEmptyPromptExists(); + if (testData.brushDeviationTargetTimestamp) { + await ml.testExecution.logTestStep('displays the no results found prompt'); + await aiops.explainLogRateSpikesPage.assertNoResultsFoundEmptyPromptExists(); - await ml.testExecution.logTestStep('adjusts the brushes to get analysis results'); - await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(false); + await ml.testExecution.logTestStep('adjusts the brushes to get analysis results'); + await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(false); - // Get the current width of the deviation brush for later comparison. - const brushSelectionWidthBefore = await aiops.explainLogRateSpikesPage.getBrushSelectionWidth( - 'aiopsBrushDeviation' - ); - - // Get the px values for the timestamp we want to move the brush to. - const { targetPx, intervalPx } = await aiops.explainLogRateSpikesPage.getPxForTimestamp( - testData.brushDeviationTargetTimestamp - ); - - // Adjust the right brush handle - await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushDeviation', - 'handle--e', - targetPx + intervalPx * testData.brushIntervalFactor - ); - - // Adjust the left brush handle - await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushDeviation', - 'handle--w', - targetPx - intervalPx * (testData.brushIntervalFactor - 1) - ); + // Get the current width of the deviation brush for later comparison. + const brushSelectionWidthBefore = + await aiops.explainLogRateSpikesPage.getBrushSelectionWidth('aiopsBrushDeviation'); - if (testData.brushBaselineTargetTimestamp) { // Get the px values for the timestamp we want to move the brush to. - const { targetPx: targetBaselinePx } = - await aiops.explainLogRateSpikesPage.getPxForTimestamp( - testData.brushBaselineTargetTimestamp - ); + const { targetPx, intervalPx } = await aiops.explainLogRateSpikesPage.getPxForTimestamp( + testData.brushDeviationTargetTimestamp + ); // Adjust the right brush handle await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushBaseline', + 'aiopsBrushDeviation', 'handle--e', - targetBaselinePx + intervalPx * testData.brushIntervalFactor + targetPx + intervalPx * testData.brushIntervalFactor ); // Adjust the left brush handle await aiops.explainLogRateSpikesPage.adjustBrushHandler( - 'aiopsBrushBaseline', + 'aiopsBrushDeviation', 'handle--w', - targetBaselinePx - intervalPx * (testData.brushIntervalFactor - 1) + targetPx - intervalPx * (testData.brushIntervalFactor - 1) ); - } - // Get the new brush selection width for later comparison. - const brushSelectionWidthAfter = await aiops.explainLogRateSpikesPage.getBrushSelectionWidth( - 'aiopsBrushDeviation' - ); + if (testData.brushBaselineTargetTimestamp) { + // Get the px values for the timestamp we want to move the brush to. + const { targetPx: targetBaselinePx } = + await aiops.explainLogRateSpikesPage.getPxForTimestamp( + testData.brushBaselineTargetTimestamp + ); + + // Adjust the right brush handle + await aiops.explainLogRateSpikesPage.adjustBrushHandler( + 'aiopsBrushBaseline', + 'handle--e', + targetBaselinePx + intervalPx * testData.brushIntervalFactor + ); - // Assert the adjusted brush: The selection width should have changed and - // we test if the selection is smaller than two bucket intervals. - // Finally, the adjusted brush should trigger - // a warning on the "Rerun analysis" button. - expect(brushSelectionWidthBefore).not.to.be(brushSelectionWidthAfter); - expect(brushSelectionWidthAfter).not.to.be.greaterThan( - intervalPx * 2 * testData.brushIntervalFactor - ); + // Adjust the left brush handle + await aiops.explainLogRateSpikesPage.adjustBrushHandler( + 'aiopsBrushBaseline', + 'handle--w', + targetBaselinePx - intervalPx * (testData.brushIntervalFactor - 1) + ); + } + + // Get the new brush selection width for later comparison. + const brushSelectionWidthAfter = + await aiops.explainLogRateSpikesPage.getBrushSelectionWidth('aiopsBrushDeviation'); + + // Assert the adjusted brush: The selection width should have changed and + // we test if the selection is smaller than two bucket intervals. + // Finally, the adjusted brush should trigger + // a warning on the "Rerun analysis" button. + expect(brushSelectionWidthBefore).not.to.be(brushSelectionWidthAfter); + expect(brushSelectionWidthAfter).not.to.be.greaterThan( + intervalPx * 2 * testData.brushIntervalFactor + ); - await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(true); + await aiops.explainLogRateSpikesPage.assertRerunAnalysisButtonExists(true); - await ml.testExecution.logTestStep('rerun the analysis with adjusted settings'); + await ml.testExecution.logTestStep('rerun the analysis with adjusted settings'); + + await aiops.explainLogRateSpikesPage.clickRerunAnalysisButton(true); + } - await aiops.explainLogRateSpikesPage.clickRerunAnalysisButton(true); await aiops.explainLogRateSpikesPage.assertProgressTitle('Progress: 100% — Done.'); // The group switch should be disabled by default @@ -178,14 +179,14 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); } - // Assert the field selector that allows to costumize grouping + await ml.testExecution.logTestStep('open the field filter'); await aiops.explainLogRateSpikesPage.assertFieldFilterPopoverButtonExists(false); await aiops.explainLogRateSpikesPage.clickFieldFilterPopoverButton(true); await aiops.explainLogRateSpikesPage.assertFieldSelectorFieldNameList( testData.expected.fieldSelectorPopover ); - // Filter fields + await ml.testExecution.logTestStep('filter fields'); await aiops.explainLogRateSpikesPage.setFieldSelectorSearch(testData.fieldSelectorSearch); await aiops.explainLogRateSpikesPage.assertFieldSelectorFieldNameList([ testData.fieldSelectorSearch, @@ -196,6 +197,7 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); if (testData.fieldSelectorApplyAvailable) { + await ml.testExecution.logTestStep('regroup results'); await aiops.explainLogRateSpikesPage.clickFieldFilterApplyButton(); if (!isTestDataExpectedWithSampleProbability(testData.expected)) { @@ -206,10 +208,33 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { ); } } + + if (testData.action !== undefined) { + await ml.testExecution.logTestStep('check all table row actions are present'); + await aiops.explainLogRateSpikesAnalysisGroupsTable.assertRowActions( + testData.action.tableRowId + ); + + await ml.testExecution.logTestStep('click log pattern analysis action'); + await aiops.explainLogRateSpikesAnalysisGroupsTable.clickRowAction( + testData.action.tableRowId, + testData.action.type + ); + + await ml.testExecution.logTestStep('check log pattern analysis page loaded correctly'); + await aiops.logPatternAnalysisPageProvider.assertLogCategorizationPageExists(); + await aiops.logPatternAnalysisPageProvider.assertTotalDocumentCount( + testData.action.expected.totalDocCount + ); + await aiops.logPatternAnalysisPageProvider.assertQueryInput( + testData.action.expected.queryBar + ); + } }); } - describe('explain log rate spikes', async function () { + // FLAKY: https://github.com/elastic/kibana/issues/155222 + describe.skip('explain log rate spikes', async function () { for (const testData of explainLogRateSpikesTestData) { describe(`with '${testData.sourceIndexOrSavedSearch}'`, function () { before(async () => { @@ -222,13 +247,27 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { await ml.testResources.setKibanaTimeZoneToUTC(); - await ml.securityUI.loginAsMlPowerUser(); + if (testData.dataGenerator === 'kibana_sample_data_logs') { + await PageObjects.security.login('elastic', 'changeme', { + expectSuccess: true, + }); + + await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { + useActualUrl: true, + }); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.home.addSampleDataSet('logs'); + await PageObjects.header.waitUntilLoadingHasFinished(); + } else { + await ml.securityUI.loginAsMlPowerUser(); + } }); after(async () => { await elasticChart.setNewChartUiDebugFlag(false); - await ml.testResources.deleteIndexPatternByTitle(testData.sourceIndexOrSavedSearch); - + if (testData.dataGenerator !== 'kibana_sample_data_logs') { + await ml.testResources.deleteIndexPatternByTitle(testData.sourceIndexOrSavedSearch); + } await aiops.explainLogRateSpikesDataGenerator.removeGeneratedData(testData.dataGenerator); }); diff --git a/x-pack/test/functional/apps/aiops/test_data.ts b/x-pack/test/functional/apps/aiops/test_data.ts index b6d3293aeba81..95e21fbfc7800 100644 --- a/x-pack/test/functional/apps/aiops/test_data.ts +++ b/x-pack/test/functional/apps/aiops/test_data.ts @@ -7,6 +7,111 @@ import type { TestData } from './types'; +export const kibanaLogsDataViewTestData: TestData = { + suiteTitle: 'kibana sample data logs', + dataGenerator: 'kibana_sample_data_logs', + isSavedSearch: false, + sourceIndexOrSavedSearch: 'kibana_sample_data_logs', + brushIntervalFactor: 1, + chartClickCoordinates: [235, 0], + fieldSelectorSearch: 'referer', + fieldSelectorApplyAvailable: true, + action: { + type: 'LogPatternAnalysis', + tableRowId: '488337254', + expected: { + queryBar: + 'clientip:30.156.16.164 AND host.keyword:elastic-elastic-elastic.org AND ip:30.156.16.163 AND response.keyword:404 AND machine.os.keyword:win xp AND geo.dest:IN AND geo.srcdest:US\\:IN', + totalDocCount: '100', + }, + }, + expected: { + totalDocCountFormatted: '14,074', + analysisGroupsTable: [ + { + group: + '* clientip: 30.156.16.164* host.keyword: elastic-elastic-elastic.org* ip: 30.156.16.163* referer: http://www.elastic-elastic-elastic.com/success/timothy-l-kopra* response.keyword: 404Showing 5 out of 8 items. 8 items unique to this group.', + docCount: '100', + }, + ], + filteredAnalysisGroupsTable: [ + { + group: + '* clientip: 30.156.16.164* host.keyword: elastic-elastic-elastic.org* ip: 30.156.16.163* response.keyword: 404* machine.os.keyword: win xpShowing 5 out of 7 items. 7 items unique to this group.', + docCount: '100', + }, + ], + analysisTable: [ + { + fieldName: 'clientip', + fieldValue: '30.156.16.164', + logRate: 'Chart type:bar chart', + pValue: '3.10e-13', + impact: 'High', + }, + { + fieldName: 'geo.dest', + fieldValue: 'IN', + logRate: 'Chart type:bar chart', + pValue: '0.000716', + impact: 'Medium', + }, + { + fieldName: 'geo.srcdest', + fieldValue: 'US:IN', + logRate: 'Chart type:bar chart', + pValue: '0.000716', + impact: 'Medium', + }, + { + fieldName: 'host.keyword', + fieldValue: 'elastic-elastic-elastic.org', + logRate: 'Chart type:bar chart', + pValue: '7.14e-9', + impact: 'High', + }, + { + fieldName: 'ip', + fieldValue: '30.156.16.163', + logRate: 'Chart type:bar chart', + pValue: '3.28e-13', + impact: 'High', + }, + { + fieldName: 'machine.os.keyword', + fieldValue: 'win xp', + logRate: 'Chart type:bar chart', + pValue: '0.0000997', + impact: 'Medium', + }, + { + fieldName: 'referer', + fieldValue: 'http://www.elastic-elastic-elastic.com/success/timothy-l-kopra', + logRate: 'Chart type:bar chart', + pValue: '4.74e-13', + impact: 'High', + }, + { + fieldName: 'response.keyword', + fieldValue: '404', + logRate: 'Chart type:bar chart', + pValue: '0.00000604', + impact: 'Medium', + }, + ], + fieldSelectorPopover: [ + 'clientip', + 'geo.dest', + 'geo.srcdest', + 'host.keyword', + 'ip', + 'machine.os.keyword', + 'referer', + 'response.keyword', + ], + }, +}; + export const farequoteDataViewTestData: TestData = { suiteTitle: 'farequote with spike', dataGenerator: 'farequote_with_spike', @@ -122,6 +227,7 @@ export const artificialLogDataViewTestData: TestData = { }; export const explainLogRateSpikesTestData: TestData[] = [ + kibanaLogsDataViewTestData, farequoteDataViewTestData, farequoteDataViewTestDataWithQuery, artificialLogDataViewTestData, diff --git a/x-pack/test/functional/apps/aiops/types.ts b/x-pack/test/functional/apps/aiops/types.ts index 01733a8e1a2af..2093d4d961363 100644 --- a/x-pack/test/functional/apps/aiops/types.ts +++ b/x-pack/test/functional/apps/aiops/types.ts @@ -7,6 +7,15 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; +interface TestDataTableActionLogPatternAnalysis { + type: 'LogPatternAnalysis'; + tableRowId: string; + expected: { + queryBar: string; + totalDocCount: string; + }; +} + interface TestDataExpectedWithSampleProbability { totalDocCountFormatted: string; sampleProbabilityFormatted: string; @@ -40,11 +49,12 @@ export interface TestData { sourceIndexOrSavedSearch: string; rowsPerPage?: 10 | 25 | 50; brushBaselineTargetTimestamp?: number; - brushDeviationTargetTimestamp: number; + brushDeviationTargetTimestamp?: number; brushIntervalFactor: number; chartClickCoordinates: [number, number]; fieldSelectorSearch: string; fieldSelectorApplyAvailable: boolean; query?: string; + action?: TestDataTableActionLogPatternAnalysis; expected: TestDataExpectedWithSampleProbability | TestDataExpectedWithoutSampleProbability; } diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index 3cf0091c93bd4..e9000a9cf3e6d 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -529,6 +529,89 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); }); + + describe('Pagination and Sorting', () => { + beforeEach(async () => { + await pageObjects.infraHostsView.changePageSize(5); + }); + + it('should show 5 rows on the first page', async () => { + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should paginate to the last page', async () => { + await pageObjects.infraHostsView.paginateTo(2); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[5])); + }); + }); + + it('should show all hosts on the same page', async () => { + await pageObjects.infraHostsView.changePageSize(10); + const hostRows = await pageObjects.infraHostsView.getHostsTableData(); + hostRows.forEach((row, position) => { + pageObjects.infraHostsView + .getHostsRowData(row) + .then((hostRowData) => expect(hostRowData).to.eql(tableEntries[position])); + }); + }); + + it('should sort by Disk Latency asc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[1]); + }); + + it('should sort by Disk Latency desc', async () => { + await pageObjects.infraHostsView.sortByDiskLatency(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[1]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + + it('should sort by Title asc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[0]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[5]); + }); + + it('should sort by Title desc', async () => { + await pageObjects.infraHostsView.sortByTitle(); + let hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataFirtPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataFirtPage).to.eql(tableEntries[5]); + + await pageObjects.infraHostsView.paginateTo(2); + hostRows = await pageObjects.infraHostsView.getHostsTableData(); + const hostDataLastPage = await pageObjects.infraHostsView.getHostsRowData(hostRows[0]); + expect(hostDataLastPage).to.eql(tableEntries[0]); + }); + }); }); }); }; diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts index ae0cc601f8cc7..6478d208226ad 100644 --- a/x-pack/test/functional/page_objects/infra_hosts_view.ts +++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts @@ -241,6 +241,7 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { async typeInQueryBar(query: string) { const queryBar = await this.getQueryBar(); + await queryBar.clearValueWithKeyboard(); return queryBar.type(query); }, @@ -249,5 +250,51 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) { await testSubjects.click('querySubmitButton'); }, + + // Pagination + getPageNumberButton(pageNumber: number) { + return testSubjects.find(`pagination-button-${pageNumber - 1}`); + }, + + getPageSizeSelector() { + return testSubjects.find('tablePaginationPopoverButton'); + }, + + getPageSizeOption(pageSize: number) { + return testSubjects.find(`tablePagination-${pageSize}-rows`); + }, + + async changePageSize(pageSize: number) { + const pageSizeSelector = await this.getPageSizeSelector(); + await pageSizeSelector.click(); + const pageSizeOption = await this.getPageSizeOption(pageSize); + await pageSizeOption.click(); + }, + + async paginateTo(pageNumber: number) { + const paginationButton = await this.getPageNumberButton(pageNumber); + await paginationButton.click(); + }, + + // Sorting + getDiskLatencyHeader() { + return testSubjects.find('tableHeaderCell_diskLatency_4'); + }, + + getTitleHeader() { + return testSubjects.find('tableHeaderCell_title_1'); + }, + + async sortByDiskLatency() { + const diskLatency = await this.getDiskLatencyHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', diskLatency); + return button.click(); + }, + + async sortByTitle() { + const titleHeader = await this.getTitleHeader(); + const button = await testSubjects.findDescendant('tableHeaderSortButton', titleHeader); + return button.click(); + }, }; } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts index 18cadebbf9afd..b533c50677944 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_analysis_groups_table.ts @@ -5,12 +5,17 @@ * 2.0. */ +import expect from '@kbn/expect'; + import { FtrProviderContext } from '../../ftr_provider_context'; export function ExplainLogRateSpikesAnalysisGroupsTableProvider({ getService, }: FtrProviderContext) { + const find = getService('find'); const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const browser = getService('browser'); return new (class AnalysisTable { public async assertSpikeAnalysisTableExists() { @@ -55,5 +60,50 @@ export function ExplainLogRateSpikesAnalysisGroupsTableProvider({ return rows; } + + public rowSelector(rowId: string, subSelector?: string) { + const row = `~aiopsSpikeAnalysisGroupsTable > ~row-${rowId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async ensureActionsMenuOpen(rowId: string) { + await retry.tryForTime(30 * 1000, async () => { + await this.ensureActionsMenuClosed(); + + if (!(await find.existsByCssSelector('.euiContextMenuPanel', 1000))) { + await testSubjects.click(this.rowSelector(rowId, 'euiCollapsedItemActionsButton')); + expect(await find.existsByCssSelector('.euiContextMenuPanel', 1000)).to.eql( + true, + 'Actions popover should exist' + ); + } + }); + } + + public async ensureActionsMenuClosed() { + await retry.tryForTime(30 * 1000, async () => { + await browser.pressKeys(browser.keys.ESCAPE); + expect(await find.existsByCssSelector('.euiContextMenuPanel', 1000)).to.eql( + false, + 'Actions popover should not exist' + ); + }); + } + + public async assertRowActions(rowId: string) { + await this.ensureActionsMenuOpen(rowId); + + await testSubjects.existOrFail('aiopsTableActionButtonCopyToClipboard enabled'); + await testSubjects.existOrFail('aiopsTableActionButtonDiscover enabled'); + await testSubjects.existOrFail('aiopsTableActionButtonLogPatternAnalysis enabled'); + + await this.ensureActionsMenuClosed(); + } + + public async clickRowAction(rowId: string, action: string) { + await this.ensureActionsMenuOpen(rowId); + await testSubjects.click(`aiopsTableActionButton${action} enabled`); + await testSubjects.missingOrFail(`aiopsTableActionButton${action} enabled`); + } })(); } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts index 1a80ac679f29b..228d47bbc746f 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_data_generator.ts @@ -122,6 +122,10 @@ export function ExplainLogRateSpikesDataGeneratorProvider({ getService }: FtrPro return new (class DataGenerator { public async generateData(dataGenerator: string) { switch (dataGenerator) { + case 'kibana_sample_data_logs': + // will be added via UI + break; + case 'farequote_with_spike': await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); @@ -191,6 +195,10 @@ export function ExplainLogRateSpikesDataGeneratorProvider({ getService }: FtrPro public async removeGeneratedData(dataGenerator: string) { switch (dataGenerator) { + case 'kibana_sample_data_logs': + // do not remove + break; + case 'farequote_with_spike': await esArchiver.unload('x-pack/test/functional/es_archives/ml/farequote'); break; diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 3da9ed7c760b7..736437a1d3976 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -229,9 +229,11 @@ export function ExplainLogRateSpikesPageProvider({ }, async assertProgressTitle(expectedProgressTitle: string) { - await testSubjects.existOrFail('aiopProgressTitle'); - const currentProgressTitle = await testSubjects.getVisibleText('aiopProgressTitle'); - expect(currentProgressTitle).to.be(expectedProgressTitle); + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopProgressTitle'); + const currentProgressTitle = await testSubjects.getVisibleText('aiopProgressTitle'); + expect(currentProgressTitle).to.be(expectedProgressTitle); + }); }, async navigateToIndexPatternSelection() { diff --git a/x-pack/test/functional/services/aiops/index.ts b/x-pack/test/functional/services/aiops/index.ts index 4816d37bcff04..8c208f182f3bd 100644 --- a/x-pack/test/functional/services/aiops/index.ts +++ b/x-pack/test/functional/services/aiops/index.ts @@ -11,6 +11,7 @@ import { ExplainLogRateSpikesPageProvider } from './explain_log_rate_spikes_page import { ExplainLogRateSpikesAnalysisTableProvider } from './explain_log_rate_spikes_analysis_table'; import { ExplainLogRateSpikesAnalysisGroupsTableProvider } from './explain_log_rate_spikes_analysis_groups_table'; import { ExplainLogRateSpikesDataGeneratorProvider } from './explain_log_rate_spikes_data_generator'; +import { LogPatternAnalysisPageProvider } from './log_pattern_analysis_page'; export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesPage = ExplainLogRateSpikesPageProvider(context); @@ -18,11 +19,13 @@ export function AiopsProvider(context: FtrProviderContext) { const explainLogRateSpikesAnalysisGroupsTable = ExplainLogRateSpikesAnalysisGroupsTableProvider(context); const explainLogRateSpikesDataGenerator = ExplainLogRateSpikesDataGeneratorProvider(context); + const logPatternAnalysisPageProvider = LogPatternAnalysisPageProvider(context); return { explainLogRateSpikesPage, explainLogRateSpikesAnalysisTable, explainLogRateSpikesAnalysisGroupsTable, explainLogRateSpikesDataGenerator, + logPatternAnalysisPageProvider, }; } diff --git a/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts new file mode 100644 index 0000000000000..37872b8d7c051 --- /dev/null +++ b/x-pack/test/functional/services/aiops/log_pattern_analysis_page.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; + +export function LogPatternAnalysisPageProvider({ getService, getPageObject }: FtrProviderContext) { + const retry = getService('retry'); + const testSubjects = getService('testSubjects'); + + return { + async assertLogCategorizationPageExists() { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail('aiopsLogCategorizationPage'); + }); + }, + + async assertQueryInput(expectedQueryString: string) { + const aiopsQueryInput = await testSubjects.find('aiopsQueryInput'); + const actualQueryString = await aiopsQueryInput.getVisibleText(); + expect(actualQueryString).to.eql( + expectedQueryString, + `Expected query bar text to be '${expectedQueryString}' (got '${actualQueryString}')` + ); + }, + + async assertTotalDocumentCount(expectedFormattedTotalDocCount: string) { + await retry.tryForTime(5000, async () => { + const docCount = await testSubjects.getVisibleText('aiopsTotalDocCount'); + expect(docCount).to.eql( + expectedFormattedTotalDocCount, + `Expected total document count to be '${expectedFormattedTotalDocCount}' (got '${docCount}')` + ); + }); + }, + }; +} diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/duplicate_exception_list.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/duplicate_exception_list.ts new file mode 100644 index 0000000000000..54e7a89042b02 --- /dev/null +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/duplicate_exception_list.ts @@ -0,0 +1,207 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { + ENDPOINT_LIST_URL, + EXCEPTION_LIST_ITEM_URL, + EXCEPTION_LIST_URL, +} from '@kbn/securitysolution-list-constants'; +import { getExceptionResponseMockWithoutAutoGeneratedValues } from '@kbn/lists-plugin/common/schemas/response/exception_list_schema.mock'; +import { getCreateExceptionListDetectionSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_schema.mock'; +import { getCreateExceptionListItemMinimalSchemaMock } from '@kbn/lists-plugin/common/schemas/request/create_exception_list_item_schema.mock'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { deleteAllExceptions, removeExceptionListServerGeneratedProperties } from '../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const log = getService('log'); + + describe('duplicate_exception_lists', () => { + afterEach(async () => { + await deleteAllExceptions(supertest, log); + }); + + it('should duplicate a list with no exception items', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + const { body } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const bodyToCompare = removeExceptionListServerGeneratedProperties(body); + expect(bodyToCompare).to.eql({ + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + type: 'detection', + list_id: body.list_id, + name: `${getCreateExceptionListDetectionSchemaMock().name} [Duplicate]`, + }); + }); + + it('should duplicate a list and its items', async () => { + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + }) + .expect(200); + + const { body: listBody } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const listBodyToCompare = removeExceptionListServerGeneratedProperties(listBody); + expect(listBodyToCompare).to.eql({ + ...getExceptionResponseMockWithoutAutoGeneratedValues(), + type: 'detection', + list_id: listBody.list_id, + name: `${getCreateExceptionListDetectionSchemaMock().name} [Duplicate]`, + }); + + expect(body.total).to.eql(1); + }); + + it('should duplicate a list with expired exception items', async () => { + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ ...getCreateExceptionListItemMinimalSchemaMock(), expire_time: expiredDate }) + .expect(200); + + const { body: listBody } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.total).to.eql(1); + }); + + it('should duplicate a list and EXCLUDE expired exception items when "include_expired_exceptions" set to "false"', async () => { + const expiredDate = new Date(Date.now() - 1000000).toISOString(); + + // create an exception list + await supertest + .post(EXCEPTION_LIST_URL) + .set('kbn-xsrf', 'true') + .send(getCreateExceptionListDetectionSchemaMock()) + .expect(200); + + await supertest + .post(EXCEPTION_LIST_ITEM_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getCreateExceptionListItemMinimalSchemaMock(), + list_id: getCreateExceptionListDetectionSchemaMock().list_id, + expire_time: expiredDate, + }) + .expect(200); + + const { body: listBody } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=${ + getCreateExceptionListDetectionSchemaMock().list_id + }&namespace_type=single&include_expired_exceptions=false` + ) + .set('kbn-xsrf', 'true') + .expect(200); + + const { body } = await supertest + .get(`${EXCEPTION_LIST_ITEM_URL}/_find?list_id=${listBody.list_id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.total).to.eql(0); + }); + + describe('error states', () => { + it('should cause a 409 if list does not exist', async () => { + const { body } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=exception_list_id&namespace_type=agnostic&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(404); + + expect(body).to.eql({ + message: 'exception list id: "exception_list_id" does not exist', + status_code: 404, + }); + }); + + it('should cause a 405 if trying to duplicate a reserved exception list type', async () => { + // create an exception list + await supertest.post(ENDPOINT_LIST_URL).set('kbn-xsrf', 'true').expect(200); + + const { body } = await supertest + .post( + `${EXCEPTION_LIST_URL}/_duplicate?list_id=endpoint_list&namespace_type=agnostic&include_expired_exceptions=true` + ) + .set('kbn-xsrf', 'true') + .expect(405); + + expect(body).to.eql({ + message: + 'unable to duplicate exception list with list_id: endpoint_list - action not allowed', + status_code: 405, + }); + }); + }); + }); +}; diff --git a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts index bfce9ac8267e7..02b63c732d229 100644 --- a/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/lists_api_integration/security_and_spaces/tests/index.ts @@ -19,6 +19,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./update_list_items')); loadTestFile(require.resolve('./delete_lists')); loadTestFile(require.resolve('./delete_list_items')); + loadTestFile(require.resolve('./duplicate_exception_list')); loadTestFile(require.resolve('./find_lists')); loadTestFile(require.resolve('./find_list_items')); loadTestFile(require.resolve('./find_lists_by_size')); diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 2bd65ef5dcd3e..dc244a51bb183 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -163,6 +163,7 @@ export default function ({ getService }: FtrProviderContext) { execution_gap_duration_s: 3000, }, }, + revision: 0, }, }, alerting: { diff --git a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts index 247b25d2be2ca..4da22a8e807a8 100644 --- a/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts +++ b/x-pack/test/screenshot_creation/apps/response_ops_docs/stack_connectors/connector_types.ts @@ -12,8 +12,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const screenshotDirectories = ['response_ops_docs', 'stack_connectors']; const pageObjects = getPageObjects(['common', 'header']); const actions = getService('actions'); - const testSubjects = getService('testSubjects'); + const browser = getService('browser'); const comboBox = getService('comboBox'); + const find = getService('find'); + const testSubjects = getService('testSubjects'); const testIndex = `test-index`; const indexDocument = `{\n` + @@ -21,13 +23,36 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { `"rule_name": "{{rule.name}}",\n` + `"alert_id": "{{alert.id}}",\n` + `"context_message": "{{context.message}}"\n`; + const emailConnectorName = 'my-email-connector'; describe('connector types', function () { + let emailConnectorId: string; + before(async () => { + ({ id: emailConnectorId } = await actions.api.createConnector({ + name: emailConnectorName, + config: { + service: 'other', + from: 'bob@example.com', + host: 'some.non.existent.com', + port: 25, + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + connectorTypeId: '.email', + })); + }); + beforeEach(async () => { await pageObjects.common.navigateToApp('connectors'); await pageObjects.header.waitUntilLoadingHasFinished(); }); + after(async () => { + await actions.api.deleteConnector(emailConnectorId); + }); + it('server log connector screenshots', async () => { await pageObjects.common.navigateToApp('connectors'); await pageObjects.header.waitUntilLoadingHasFinished(); @@ -57,5 +82,43 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const flyOutCancelButton = await testSubjects.find('euiFlyoutCloseButton'); await flyOutCancelButton.click(); }); + + it('email connector screenshots', async () => { + await pageObjects.common.navigateToApp('connectors'); + await pageObjects.header.waitUntilLoadingHasFinished(); + await actions.common.openNewConnectorForm('email'); + await testSubjects.setValue('nameInput', 'Gmail connector'); + await testSubjects.setValue('emailFromInput', 'test@gmail.com'); + await testSubjects.setValue('emailServiceSelectInput', 'gmail'); + await commonScreenshots.takeScreenshot('email-connector', screenshotDirectories); + const flyOutCancelButton = await testSubjects.find('euiFlyoutCloseButton'); + await flyOutCancelButton.click(); + }); + + it('test email connector screenshots', async () => { + const searchBox = await find.byCssSelector('[data-test-subj="actionsList"] .euiFieldSearch'); + await searchBox.click(); + await searchBox.clearValue(); + await searchBox.type('my actionTypeId:(.email)'); + await searchBox.pressKeys(browser.keys.ENTER); + const connectorList = await testSubjects.find('actionsTable'); + const emailConnector = await connectorList.findByCssSelector( + `[title="${emailConnectorName}"]` + ); + await emailConnector.click(); + const testButton = await testSubjects.find('testConnectorTab'); + await testButton.click(); + await testSubjects.setValue('comboBoxSearchInput', 'elastic@gmail.com'); + await testSubjects.setValue('subjectInput', 'Test subject'); + await testSubjects.setValue('messageTextArea', 'Enter message text'); + /* timing issue sometimes happens with the combobox so we just try to set the subjectInput again */ + await testSubjects.setValue('subjectInput', 'Test subject'); + await commonScreenshots.takeScreenshot( + 'email-params-test', + screenshotDirectories, + 1400, + 1024 + ); + }); }); } 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 5d19932090168..735285753ea23 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 @@ -33,27 +33,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Actions', ], [ - 'Host-dpu1a2r2yi', + 'Host-9qenwrl9ko', 'x', 'x', 'Warning', - 'macOS', - '10.2.17.24, 10.56.215.200,10.254.196.130', - 'x', - 'x', - '', - ], - [ - 'Host-rs9wp4o6l9', - 'x', - 'x', - 'Success', 'Linux', - '10.138.79.131, 10.170.160.154', + '10.56.228.101, 10.201.120.140,10.236.180.146', 'x', 'x', '', ], + ['Host-qw2bti801m', 'x', 'x', 'Failure', 'macOS', '10.244.59.227', 'x', 'x', ''], [ 'Host-u5jy6j0pwb', 'x', diff --git a/yarn.lock b/yarn.lock index 5ab19eee75bd9..d120721650856 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4545,6 +4545,10 @@ version "0.0.0" uid "" +"@kbn/ml-error-utils@link:x-pack/packages/ml/error_utils": + version "0.0.0" + uid "" + "@kbn/ml-is-defined@link:x-pack/packages/ml/is_defined": version "0.0.0" uid "" @@ -5457,6 +5461,10 @@ version "0.0.0" uid "" +"@kbn/url-state@link:packages/kbn-url-state": + version "0.0.0" + uid "" + "@kbn/usage-collection-plugin@link:src/plugins/usage_collection": version "0.0.0" uid ""