diff --git a/.ci/package-testing/Jenkinsfile b/.ci/package-testing/Jenkinsfile new file mode 100644 index 00000000000000..b749c1fe2e9af0 --- /dev/null +++ b/.ci/package-testing/Jenkinsfile @@ -0,0 +1,29 @@ +#!/bin/groovy +library 'kibana-pipeline-library' +kibanaLibrary.load() +kibanaPipeline(timeoutMinutes: 300) { + slackNotifications.onFailure { + ciStats.trackBuild { + workers.ci(ramDisk: false, name: "package-build", size: 'l', runErrorReporter: false) { + withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { + kibanaPipeline.bash("test/scripts/jenkins_xpack_package_build.sh", "Package builds") + } + } + def packageTypes = ['deb', 'docker', 'rpm'] + def workers = [:] + packageTypes.each { type -> + workers["package-${type}"] = { + testPackage(type) + } + } + parallel(workers) + } + } +} +def testPackage(packageType) { + workers.ci(ramDisk: false, name: "package-${packageType}", size: 's', runErrorReporter: false) { + withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') { + kibanaPipeline.bash("test/scripts/jenkins_xpack_package_${packageType}.sh", "Execute package testing for ${packageType}") + } + } +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 39daa5780436f3..9c9200ce33e7ae 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -94,9 +94,6 @@ /x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @elastic/uptime /x-pack/test/apm_api_integration/tests/csm/ @elastic/uptime -# Beats -/x-pack/plugins/beats_management/ @elastic/beats -#CC# /x-pack/plugins/beats_management/ @elastic/beats # Presentation /src/plugins/dashboard/ @elastic/kibana-presentation @@ -150,6 +147,7 @@ #CC# /x-pack/plugins/file_upload @elastic/kibana-gis /src/plugins/tile_map/ @elastic/kibana-gis /src/plugins/region_map/ @elastic/kibana-gis +/packages/kbn-mapbox-gl @elastic/kibana-gis # Operations /src/dev/ @elastic/kibana-operations diff --git a/.github/ISSUE_TEMPLATE/v8_breaking_change.md b/.github/ISSUE_TEMPLATE/v8_breaking_change.md index 67d2ee2d3286b4..9361cf7a52b654 100644 --- a/.github/ISSUE_TEMPLATE/v8_breaking_change.md +++ b/.github/ISSUE_TEMPLATE/v8_breaking_change.md @@ -13,7 +13,8 @@ assignees: '' **************************************** Please add a team label to denote the team that the -breaking change is applicable to. +breaking change is applicable to. If the work requires +changes to Upgrade Assistant itself, please tag Team:Elasticsearch UI. --> @@ -23,6 +24,10 @@ breaking change is applicable to. 8.0 +**Is this a Kibana or Elasticsearch breaking change?** + + + **Describe the change. How will it manifest to users?** **How many users will be affected?** @@ -30,7 +35,9 @@ breaking change is applicable to. -**Can the change be registered with the [Kibana deprecation service](https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md)?** +**Are there any edge cases?** + +**[For Kibana deprecations] Can the change be registered with the [Kibana deprecation service](https://github.com/elastic/kibana/blob/master/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md)?** -**Are there any edge cases?** +**[For Elasticsearch deprecations] Can the Upgrade Assistant make the migration easier for users? Please explain the proposed solution in as much detail as possible.** + + + ## Test Data diff --git a/.gitignore b/.gitignore index ce8fd38b18a929..f9855520cb1103 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ npm-debug.log* .ci/runbld .ci/bash_standard_lib.sh .gradle +.vagrant ## @cypress/snapshot from apm plugin snapshots.js diff --git a/.i18nrc.json b/.i18nrc.json index 1e07d662c057a6..dc01a10b6a686b 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -1,6 +1,5 @@ { "paths": { - "common.ui": "src/legacy/ui", "console": "src/plugins/console", "core": "src/core", "discover": "src/plugins/discover", @@ -61,6 +60,5 @@ "apmOss": "src/plugins/apm_oss", "usageCollection": "src/plugins/usage_collection" }, - "exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"], "translations": [] } diff --git a/api_docs/alerting.json b/api_docs/alerting.json index ddb92f5aff9bb1..13a150d0af00da 100644 --- a/api_docs/alerting.json +++ b/api_docs/alerting.json @@ -516,13 +516,15 @@ "label": "rule", "description": [], "signature": [ + "Pick, \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">, \"enabled\" | \"name\" | \"actions\" | \"tags\" | \"consumer\" | \"schedule\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"throttle\" | \"notifyWhen\"> & { producer: string; ruleTypeId: string; ruleTypeName: string; }" ], "source": { "path": "x-pack/plugins/alerting/server/types.ts", @@ -840,7 +842,7 @@ "description": [], "source": { "path": "x-pack/plugins/alerting/server/types.ts", - "lineNumber": 75 + "lineNumber": 76 }, "deprecated": false } @@ -1030,7 +1032,7 @@ ], "source": { "path": "x-pack/plugins/alerting/server/types.ts", - "lineNumber": 107 + "lineNumber": 109 }, "deprecated": false } @@ -3913,7 +3915,15 @@ "label": "SanitizedRuleConfig", "description": [], "signature": [ - "Pick, \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"muteAll\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"mutedInstanceIds\" | \"executionStatus\">, \"enabled\" | \"name\" | \"actions\" | \"tags\" | \"consumer\" | \"schedule\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"throttle\" | \"notifyWhen\"> & { producer: string; ruleTypeId: string; ruleTypeName: string; }" + "Pick, \"enabled\" | \"id\" | \"name\" | \"params\" | \"actions\" | \"tags\" | \"alertTypeId\" | \"consumer\" | \"schedule\" | \"scheduledTaskId\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"apiKeyOwner\" | \"throttle\" | \"notifyWhen\" | \"muteAll\" | \"mutedInstanceIds\" | \"executionStatus\">, \"enabled\" | \"name\" | \"actions\" | \"tags\" | \"consumer\" | \"schedule\" | \"createdBy\" | \"updatedBy\" | \"createdAt\" | \"updatedAt\" | \"throttle\" | \"notifyWhen\"> & { producer: string; ruleTypeId: string; ruleTypeName: string; }" ], "source": { "path": "x-pack/plugins/alerting/common/alert.ts", diff --git a/api_docs/beats_management.json b/api_docs/beats_management.json deleted file mode 100644 index c8f0f6af96ce8a..00000000000000 --- a/api_docs/beats_management.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "id": "beatsManagement", - "client": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "server": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "common": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [ - { - "parentPluginId": "beatsManagement", - "id": "def-common.BeatsManagementConfigType", - "type": "Type", - "tags": [], - "label": "BeatsManagementConfigType", - "description": [], - "signature": [ - "{ readonly enabled: boolean; readonly defaultUserRoles: string[]; readonly encryptionKey: string; readonly enrollmentTokensTtlInSeconds: number; }" - ], - "source": { - "path": "x-pack/plugins/beats_management/common/index.ts", - "lineNumber": 23 - }, - "deprecated": false, - "initialIsOpen": false - } - ], - "objects": [ - { - "parentPluginId": "beatsManagement", - "id": "def-common.beatsManagementConfigSchema", - "type": "Object", - "tags": [], - "label": "beatsManagementConfigSchema", - "description": [], - "signature": [ - "ObjectType", - "<{ enabled: ", - "Type", - "; defaultUserRoles: ", - "Type", - "; encryptionKey: ", - "Type", - "; enrollmentTokensTtlInSeconds: ", - "Type", - "; }>" - ], - "source": { - "path": "x-pack/plugins/beats_management/common/index.ts", - "lineNumber": 12 - }, - "deprecated": false, - "initialIsOpen": false - } - ] - } -} \ No newline at end of file diff --git a/api_docs/beats_management.mdx b/api_docs/beats_management.mdx deleted file mode 100644 index 9d712c102a1a29..00000000000000 --- a/api_docs/beats_management.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -id: kibBeatsManagementPluginApi -slug: /kibana-dev-docs/beatsManagementPluginApi -title: beatsManagement -image: https://source.unsplash.com/400x175/?github -summary: API docs for the beatsManagement plugin -date: 2020-11-16 -tags: ['contributor', 'dev', 'apidocs', 'kibana', 'beatsManagement'] -warning: 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. ---- - -import beatsManagementObj from './beats_management.json'; - -## Common - -### Objects - - -### Consts, variables and types - - diff --git a/api_docs/core.json b/api_docs/core.json index fbf766cad30b38..ee4fe9b10b145e 100644 --- a/api_docs/core.json +++ b/api_docs/core.json @@ -1112,7 +1112,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 206 + "lineNumber": 203 }, "deprecated": false, "children": [ @@ -1136,7 +1136,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 208 + "lineNumber": 205 }, "deprecated": false }, @@ -1160,7 +1160,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 210 + "lineNumber": 207 }, "deprecated": false }, @@ -1184,7 +1184,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 212 + "lineNumber": 209 }, "deprecated": false }, @@ -1208,7 +1208,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 214 + "lineNumber": 211 }, "deprecated": false }, @@ -1232,7 +1232,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 216 + "lineNumber": 213 }, "deprecated": false }, @@ -1252,7 +1252,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 223 + "lineNumber": 220 }, "deprecated": true, "references": [] @@ -1279,7 +1279,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 227 + "lineNumber": 224 }, "deprecated": false, "returnComment": [], @@ -1299,7 +1299,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 251 + "lineNumber": 248 }, "deprecated": false, "children": [ @@ -1323,7 +1323,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 253 + "lineNumber": 250 }, "deprecated": false }, @@ -1347,7 +1347,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 255 + "lineNumber": 252 }, "deprecated": false }, @@ -1371,7 +1371,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 257 + "lineNumber": 254 }, "deprecated": false }, @@ -1395,7 +1395,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 259 + "lineNumber": 256 }, "deprecated": false }, @@ -1419,7 +1419,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 261 + "lineNumber": 258 }, "deprecated": false }, @@ -1443,7 +1443,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 263 + "lineNumber": 260 }, "deprecated": false }, @@ -1467,7 +1467,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 265 + "lineNumber": 262 }, "deprecated": false }, @@ -1491,7 +1491,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 267 + "lineNumber": 264 }, "deprecated": false }, @@ -1515,7 +1515,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 269 + "lineNumber": 266 }, "deprecated": false }, @@ -1539,7 +1539,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 271 + "lineNumber": 268 }, "deprecated": false }, @@ -1563,7 +1563,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 273 + "lineNumber": 270 }, "deprecated": false }, @@ -1583,7 +1583,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 280 + "lineNumber": 277 }, "deprecated": true, "references": [ @@ -7427,7 +7427,7 @@ ], "source": { "path": "src/core/public/index.ts", - "lineNumber": 237 + "lineNumber": 234 }, "deprecated": false, "initialIsOpen": false @@ -8545,45 +8545,38 @@ } }, { - "plugin": "securitySolution", + "plugin": "monitoring", "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 125 + "path": "x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts", + "lineNumber": 107 } }, { - "plugin": "securitySolution", + "plugin": "monitoring", "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 218 + "path": "x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts", + "lineNumber": 148 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 235 - } - }, - { - "plugin": "beatsManagement", - "link": { - "path": "x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts", - "lineNumber": 113 + "lineNumber": 125 } }, { - "plugin": "monitoring", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/monitoring/server/kibana_monitoring/collectors/get_usage_collector.ts", - "lineNumber": 107 + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 218 } }, { - "plugin": "monitoring", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/monitoring/server/telemetry_collection/register_monitoring_telemetry_collection.ts", - "lineNumber": 148 + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 235 } }, { @@ -9387,55 +9380,6 @@ "lineNumber": 67 } }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/server/routes/read_privileges_route.ts", - "lineNumber": 31 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/server/routes/read_privileges_route.ts", - "lineNumber": 35 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts", - "lineNumber": 34 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts", - "lineNumber": 52 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts", - "lineNumber": 60 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts", - "lineNumber": 41 - } - }, - { - "plugin": "beatsManagement", - "link": { - "path": "x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts", - "lineNumber": 111 - } - }, { "plugin": "canvas", "link": { @@ -9821,6 +9765,20 @@ "lineNumber": 420 } }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/server/routes/read_privileges_route.ts", + "lineNumber": 31 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/server/routes/read_privileges_route.ts", + "lineNumber": 35 + } + }, { "plugin": "monitoring", "link": { @@ -9842,6 +9800,34 @@ "lineNumber": 356 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/framework/kibana_framework_adapter.ts", + "lineNumber": 34 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/query_signals_route.ts", + "lineNumber": 52 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts", + "lineNumber": 60 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/privileges/read_privileges_route.ts", + "lineNumber": 41 + } + }, { "plugin": "canvas", "link": { @@ -12712,34 +12698,6 @@ "lineNumber": 117 } }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 125 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 218 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 235 - } - }, - { - "plugin": "beatsManagement", - "link": { - "path": "x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts", - "lineNumber": 31 - } - }, { "plugin": "indexManagement", "link": { @@ -12768,6 +12726,27 @@ "lineNumber": 41 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 125 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 218 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 235 + } + }, { "plugin": "licensing", "link": { @@ -15940,48 +15919,6 @@ "deprecated": true, "removeBy": "7.16", "references": [ - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts", - "lineNumber": 9 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts", - "lineNumber": 27 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts", - "lineNumber": 31 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 10 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 447 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", - "lineNumber": 466 - } - }, { "plugin": "canvas", "link": { @@ -16269,6 +16206,48 @@ "lineNumber": 35 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 10 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 447 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/lib/telemetry/sender.ts", + "lineNumber": 466 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts", + "lineNumber": 9 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts", + "lineNumber": 27 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts", + "lineNumber": 31 + } + }, { "plugin": "monitoring", "link": { @@ -16458,6 +16437,27 @@ "lineNumber": 20 } }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/target/types/server/schemas/common/get_call_cluster.mock.d.ts", + "lineNumber": 2 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/target/types/server/schemas/common/get_call_cluster.mock.d.ts", + "lineNumber": 4 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/target/types/server/schemas/common/get_call_cluster.mock.d.ts", + "lineNumber": 5 + } + }, { "plugin": "monitoring", "link": { @@ -19544,7 +19544,7 @@ "The os platform" ], "signature": [ - "\"linux\" | \"aix\" | \"android\" | \"darwin\" | \"freebsd\" | \"openbsd\" | \"sunos\" | \"win32\" | \"cygwin\" | \"netbsd\"" + "\"linux\" | \"aix\" | \"android\" | \"darwin\" | \"freebsd\" | \"openbsd\" | \"sunos\" | \"win32\" | \"cygwin\"" ], "source": { "path": "src/core/server/metrics/collectors/types.ts", @@ -23173,20 +23173,6 @@ "lineNumber": 180 } }, - { - "plugin": "beatsManagement", - "link": { - "path": "x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts", - "lineNumber": 8 - } - }, - { - "plugin": "beatsManagement", - "link": { - "path": "x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts", - "lineNumber": 28 - } - }, { "plugin": "licensing", "link": { diff --git a/api_docs/core_application.json b/api_docs/core_application.json index 3141bf16a93498..13b110900ab530 100644 --- a/api_docs/core_application.json +++ b/api_docs/core_application.json @@ -886,7 +886,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 339 + "lineNumber": 326 }, "deprecated": false } @@ -930,26 +930,44 @@ }, { "parentPluginId": "core", - "id": "def-public.App.meta", - "type": "Object", + "id": "def-public.App.keywords", + "type": "Array", + "tags": [], + "label": "keywords", + "description": [ + "Optional keywords to match with in deep links search. Omit if this part of the hierarchy does not have a page URL." + ], + "signature": [ + "string[] | undefined" + ], + "source": { + "path": "src/core/public/application/types.ts", + "lineNumber": 215 + }, + "deprecated": false + }, + { + "parentPluginId": "core", + "id": "def-public.App.deepLinks", + "type": "Array", "tags": [], - "label": "meta", + "label": "deepLinks", "description": [ - "\nMeta data for an application that represent additional information for the app.\nSee {@link AppMeta}\n" + "\nInput type for registering secondary in-app locations for an application.\n\nDeep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path`\nrepresents a topological level in the application's hierarchy, but does not have a destination URL that is\nuser-accessible.\n" ], "signature": [ { "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppMeta", - "text": "AppMeta" + "section": "def-public.AppDeepLink", + "text": "AppDeepLink" }, - " | undefined" + "[] | undefined" ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 244 + "lineNumber": 254 }, "deprecated": false } @@ -967,7 +985,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 565 + "lineNumber": 552 }, "deprecated": false, "children": [ @@ -990,7 +1008,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 566 + "lineNumber": 553 }, "deprecated": false }, @@ -1003,7 +1021,7 @@ "description": [], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 567 + "lineNumber": 554 }, "deprecated": false }, @@ -1019,7 +1037,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 568 + "lineNumber": 555 }, "deprecated": false }, @@ -1035,7 +1053,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 569 + "lineNumber": 556 }, "deprecated": false } @@ -1053,7 +1071,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 553 + "lineNumber": 540 }, "deprecated": false, "children": [ @@ -1076,7 +1094,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 554 + "lineNumber": 541 }, "deprecated": false } @@ -1092,7 +1110,7 @@ "description": [], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 620 + "lineNumber": 607 }, "deprecated": false, "children": [ @@ -1118,7 +1136,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 626 + "lineNumber": 613 }, "deprecated": false, "children": [ @@ -1143,7 +1161,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 626 + "lineNumber": 613 }, "deprecated": false, "isRequired": true @@ -1175,7 +1193,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 655 + "lineNumber": 642 }, "deprecated": false, "children": [ @@ -1200,7 +1218,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 655 + "lineNumber": 642 }, "deprecated": false, "isRequired": true @@ -1220,7 +1238,7 @@ "description": [], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 696 + "lineNumber": 683 }, "deprecated": false, "children": [ @@ -1238,7 +1256,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 700 + "lineNumber": 687 }, "deprecated": false }, @@ -1265,7 +1283,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 709 + "lineNumber": 696 }, "deprecated": false }, @@ -1291,7 +1309,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 717 + "lineNumber": 704 }, "deprecated": false, "children": [ @@ -1307,7 +1325,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 717 + "lineNumber": 704 }, "deprecated": false, "isRequired": true @@ -1333,7 +1351,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 717 + "lineNumber": 704 }, "deprecated": false, "isRequired": false @@ -1355,7 +1373,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 755 + "lineNumber": 742 }, "deprecated": false, "children": [ @@ -1373,7 +1391,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 755 + "lineNumber": 742 }, "deprecated": false, "isRequired": true @@ -1395,7 +1413,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 769 + "lineNumber": 756 }, "deprecated": false, "children": [ @@ -1411,7 +1429,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 769 + "lineNumber": 756 }, "deprecated": false, "isRequired": true @@ -1425,7 +1443,7 @@ "description": [], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 769 + "lineNumber": 756 }, "deprecated": false, "children": [ @@ -1441,7 +1459,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 769 + "lineNumber": 756 }, "deprecated": false }, @@ -1457,7 +1475,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 769 + "lineNumber": 756 }, "deprecated": false } @@ -1481,68 +1499,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 774 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "core", - "id": "def-public.AppMeta", - "type": "Interface", - "tags": [], - "label": "AppMeta", - "description": [ - "\nInput type for meta data for an application.\n\nMeta fields include `keywords` and `searchDeepLinks`\nKeywords is an array of string with which to associate the app, must include at least one unique string as an array.\n`searchDeepLinks` is an array of links that represent secondary in-app locations for the app." - ], - "source": { - "path": "src/core/public/application/types.ts", - "lineNumber": 255 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "core", - "id": "def-public.AppMeta.keywords", - "type": "Array", - "tags": [], - "label": "keywords", - "description": [ - "Keywords to represent this application" - ], - "signature": [ - "string[] | undefined" - ], - "source": { - "path": "src/core/public/application/types.ts", - "lineNumber": 257 - }, - "deprecated": false - }, - { - "parentPluginId": "core", - "id": "def-public.AppMeta.searchDeepLinks", - "type": "Array", - "tags": [], - "label": "searchDeepLinks", - "description": [ - "Array of links that represent secondary in-app locations for the app." - ], - "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppSearchDeepLink", - "text": "AppSearchDeepLink" - }, - "[] | undefined" - ], - "source": { - "path": "src/core/public/application/types.ts", - "lineNumber": 259 + "lineNumber": 761 }, "deprecated": false } @@ -1568,7 +1525,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 349 + "lineNumber": 336 }, "deprecated": false, "children": [ @@ -1586,7 +1543,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 353 + "lineNumber": 340 }, "deprecated": false }, @@ -1611,7 +1568,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 399 + "lineNumber": 386 }, "deprecated": false }, @@ -1628,7 +1585,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 449 + "lineNumber": 436 }, "deprecated": true, "references": [ @@ -1721,7 +1678,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 484 + "lineNumber": 471 }, "deprecated": true, "references": [ @@ -1746,20 +1703,6 @@ "lineNumber": 298 } }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/app_plugin/types.ts", - "lineNumber": 71 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/app_plugin/mounter.tsx", - "lineNumber": 171 - } - }, { "plugin": "maps", "link": { @@ -1781,6 +1724,20 @@ "lineNumber": 19 } }, + { + "plugin": "lens", + "link": { + "path": "x-pack/plugins/lens/public/app_plugin/types.ts", + "lineNumber": 71 + } + }, + { + "plugin": "lens", + "link": { + "path": "x-pack/plugins/lens/public/app_plugin/mounter.tsx", + "lineNumber": 171 + } + }, { "plugin": "ml", "link": { @@ -1882,7 +1839,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 484 + "lineNumber": 471 }, "deprecated": false } @@ -1910,7 +1867,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 515 + "lineNumber": 502 }, "deprecated": false, "returnComment": [], @@ -1934,7 +1891,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 515 + "lineNumber": 502 }, "deprecated": false } @@ -1954,7 +1911,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 674 + "lineNumber": 661 }, "deprecated": false, "children": [ @@ -1972,7 +1929,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 679 + "lineNumber": 666 }, "deprecated": false }, @@ -1990,7 +1947,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 683 + "lineNumber": 670 }, "deprecated": false }, @@ -2008,7 +1965,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 687 + "lineNumber": 674 }, "deprecated": false }, @@ -2026,7 +1983,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 692 + "lineNumber": 679 }, "deprecated": false } @@ -2046,7 +2003,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 540 + "lineNumber": 527 }, "deprecated": false, "initialIsOpen": false @@ -2085,6 +2042,57 @@ } ], "misc": [ + { + "parentPluginId": "core", + "id": "def-public.AppDeepLink", + "type": "Type", + "tags": [], + "label": "AppDeepLink", + "description": [ + "\nInput type for registering secondary in-app locations for an application.\n\nDeep links must include at least one of `path` or `deepLinks`. A deep link that does not have a `path`\nrepresents a topological level in the application's hierarchy, but does not have a destination URL that is\nuser-accessible." + ], + "signature": [ + "({ id: string; title: string; keywords?: string[] | undefined; navLinkStatus?: ", + { + "pluginId": "core", + "scope": "public", + "docId": "kibCoreApplicationPluginApi", + "section": "def-public.AppNavLinkStatus", + "text": "AppNavLinkStatus" + }, + " | undefined; } & { path: string; deepLinks?: ", + { + "pluginId": "core", + "scope": "public", + "docId": "kibCoreApplicationPluginApi", + "section": "def-public.AppDeepLink", + "text": "AppDeepLink" + }, + "[] | undefined; }) | ({ id: string; title: string; keywords?: string[] | undefined; navLinkStatus?: ", + { + "pluginId": "core", + "scope": "public", + "docId": "kibCoreApplicationPluginApi", + "section": "def-public.AppNavLinkStatus", + "text": "AppNavLinkStatus" + }, + " | undefined; } & { path?: string | undefined; deepLinks: ", + { + "pluginId": "core", + "scope": "public", + "docId": "kibCoreApplicationPluginApi", + "section": "def-public.AppDeepLink", + "text": "AppDeepLink" + }, + "[]; })" + ], + "source": { + "path": "src/core/public/application/types.ts", + "lineNumber": 279 + }, + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-public.AppLeaveAction", @@ -2113,7 +2121,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 579 + "lineNumber": 566 }, "deprecated": false, "initialIsOpen": false @@ -2143,7 +2151,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 530 + "lineNumber": 517 }, "deprecated": true, "references": [ @@ -2251,42 +2259,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 338 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "core", - "id": "def-public.AppSearchDeepLink", - "type": "Type", - "tags": [], - "label": "AppSearchDeepLink", - "description": [ - "\nInput type for registering secondary in-app locations for an application.\n\nDeep links must include at least one of `path` or `searchDeepLinks`. A deep link that does not have a `path`\nrepresents a topological level in the application's hierarchy, but does not have a destination URL that is\nuser-accessible." - ], - "signature": [ - "({ id: string; title: string; } & { path: string; searchDeepLinks?: ", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppSearchDeepLink", - "text": "AppSearchDeepLink" - }, - "[] | undefined; keywords?: string[] | undefined; }) | ({ id: string; title: string; } & { path?: string | undefined; searchDeepLinks: ", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppSearchDeepLink", - "text": "AppSearchDeepLink" - }, - "[]; keywords?: string[] | undefined; })" - ], - "source": { - "path": "src/core/public/application/types.ts", - "lineNumber": 293 + "lineNumber": 325 }, "deprecated": false, "initialIsOpen": false @@ -2305,7 +2278,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 346 + "lineNumber": 333 }, "deprecated": false, "initialIsOpen": false @@ -2328,15 +2301,15 @@ "section": "def-public.AppStatus", "text": "AppStatus" }, - " | undefined; meta?: ", + " | undefined; deepLinks?: ", { "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppMeta", - "text": "AppMeta" + "section": "def-public.AppDeepLink", + "text": "AppDeepLink" }, - " | undefined; navLinkStatus?: ", + "[] | undefined; navLinkStatus?: ", { "pluginId": "core", "scope": "public", @@ -2379,7 +2352,7 @@ "section": "def-public.App", "text": "App" }, - ", \"status\" | \"meta\" | \"navLinkStatus\" | \"defaultPath\" | \"tooltip\">> | undefined" + ", \"status\" | \"deepLinks\" | \"navLinkStatus\" | \"defaultPath\" | \"tooltip\">> | undefined" ], "source": { "path": "src/core/public/application/types.ts", @@ -2390,12 +2363,12 @@ }, { "parentPluginId": "core", - "id": "def-public.PublicAppInfo", + "id": "def-public.PublicAppDeepLinkInfo", "type": "Type", "tags": [], - "label": "PublicAppInfo", + "label": "PublicAppDeepLinkInfo", "description": [ - "\nPublic information about a registered {@link App | application}\n" + "\nPublic information about a registered app's {@link AppDeepLink | deepLinks}\n" ], "signature": [ "Pick<", @@ -2403,18 +2376,18 @@ "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.App", - "text": "App" + "section": "def-public.AppDeepLink", + "text": "AppDeepLink" }, - ", \"status\" | \"title\" | \"id\" | \"order\" | \"category\" | \"navLinkStatus\" | \"defaultPath\" | \"tooltip\" | \"euiIconType\" | \"icon\" | \"capabilities\" | \"chromeless\" | \"appRoute\" | \"exactRoute\"> & { status: ", + ", \"title\" | \"id\" | \"path\"> & { deepLinks: ", { "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppStatus", - "text": "AppStatus" + "section": "def-public.PublicAppDeepLinkInfo", + "text": "PublicAppDeepLinkInfo" }, - "; navLinkStatus: ", + "[]; keywords: string[]; navLinkStatus: ", { "pluginId": "core", "scope": "public", @@ -2422,31 +2395,23 @@ "section": "def-public.AppNavLinkStatus", "text": "AppNavLinkStatus" }, - "; appRoute: string; meta: ", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreApplicationPluginApi", - "section": "def-public.PublicAppMetaInfo", - "text": "PublicAppMetaInfo" - }, "; }" ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 322 + "lineNumber": 262 }, "deprecated": false, "initialIsOpen": false }, { "parentPluginId": "core", - "id": "def-public.PublicAppMetaInfo", + "id": "def-public.PublicAppInfo", "type": "Type", "tags": [], - "label": "PublicAppMetaInfo", + "label": "PublicAppInfo", "description": [ - "\nPublic information about a registered app's {@link AppMeta | keywords }\n" + "\nPublic information about a registered {@link App | application}\n" ], "signature": [ "Pick<", @@ -2454,57 +2419,38 @@ "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppMeta", - "text": "AppMeta" + "section": "def-public.App", + "text": "App" }, - ", never> & { keywords: string[]; searchDeepLinks: ", + ", \"status\" | \"title\" | \"id\" | \"order\" | \"category\" | \"navLinkStatus\" | \"defaultPath\" | \"tooltip\" | \"euiIconType\" | \"icon\" | \"capabilities\" | \"chromeless\" | \"appRoute\" | \"exactRoute\"> & { status: ", { "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.PublicAppSearchDeepLinkInfo", - "text": "PublicAppSearchDeepLinkInfo" + "section": "def-public.AppStatus", + "text": "AppStatus" }, - "[]; }" - ], - "source": { - "path": "src/core/public/application/types.ts", - "lineNumber": 267 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "core", - "id": "def-public.PublicAppSearchDeepLinkInfo", - "type": "Type", - "tags": [], - "label": "PublicAppSearchDeepLinkInfo", - "description": [ - "\nPublic information about a registered app's {@link AppSearchDeepLink | searchDeepLinks}\n" - ], - "signature": [ - "Pick<", + "; navLinkStatus: ", { "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppSearchDeepLink", - "text": "AppSearchDeepLink" + "section": "def-public.AppNavLinkStatus", + "text": "AppNavLinkStatus" }, - ", \"title\" | \"id\" | \"path\"> & { searchDeepLinks: ", + "; appRoute: string; keywords: string[]; deepLinks: ", { "pluginId": "core", "scope": "public", "docId": "kibCoreApplicationPluginApi", - "section": "def-public.PublicAppSearchDeepLinkInfo", - "text": "PublicAppSearchDeepLinkInfo" + "section": "def-public.PublicAppDeepLinkInfo", + "text": "PublicAppDeepLinkInfo" }, - "[]; keywords: string[]; }" + "[]; }" ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 277 + "lineNumber": 308 }, "deprecated": false, "initialIsOpen": false diff --git a/api_docs/core_chrome.json b/api_docs/core_chrome.json index d6ec4b0d3f640b..fae65ff62c9714 100644 --- a/api_docs/core_chrome.json +++ b/api_docs/core_chrome.json @@ -890,7 +890,7 @@ "description": [], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 15 + "lineNumber": 14 }, "deprecated": false, "children": [ @@ -905,7 +905,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 19 + "lineNumber": 18 }, "deprecated": false }, @@ -920,7 +920,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 24 + "lineNumber": 23 }, "deprecated": false }, @@ -939,7 +939,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 29 + "lineNumber": 28 }, "deprecated": false }, @@ -954,7 +954,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 34 + "lineNumber": 33 }, "deprecated": false }, @@ -972,7 +972,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 40 + "lineNumber": 39 }, "deprecated": false }, @@ -990,7 +990,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 45 + "lineNumber": 44 }, "deprecated": false }, @@ -1008,7 +1008,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 50 + "lineNumber": 49 }, "deprecated": false }, @@ -1026,7 +1026,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 56 + "lineNumber": 55 }, "deprecated": false }, @@ -1044,7 +1044,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 62 + "lineNumber": 61 }, "deprecated": false }, @@ -1059,7 +1059,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 67 + "lineNumber": 66 }, "deprecated": false }, @@ -1077,7 +1077,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 76 + "lineNumber": 75 }, "deprecated": false }, @@ -1095,7 +1095,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 81 + "lineNumber": 80 }, "deprecated": false } @@ -1297,103 +1297,6 @@ ], "returnComment": [] }, - { - "parentPluginId": "core", - "id": "def-public.ChromeNavLinks.update", - "type": "Function", - "tags": [ - "deprecated" - ], - "label": "update", - "description": [ - "\nUpdate the navlink for the given id with the updated attributes.\nReturns the updated navlink or `undefined` if it does not exist.\n" - ], - "signature": [ - "(id: string, values: Partial>) => ", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreChromePluginApi", - "section": "def-public.ChromeNavLink", - "text": "ChromeNavLink" - }, - " | undefined" - ], - "source": { - "path": "src/core/public/chrome/nav_links/nav_links_service.ts", - "lineNumber": 71 - }, - "deprecated": true, - "references": [ - { - "plugin": "apm", - "link": { - "path": "x-pack/plugins/apm/public/toggleAppLinkInNav.ts", - "lineNumber": 13 - } - }, - { - "plugin": "graph", - "link": { - "path": "x-pack/plugins/graph/public/services/toggle_nav_link.ts", - "lineNumber": 26 - } - } - ], - "children": [ - { - "parentPluginId": "core", - "id": "def-public.ChromeNavLinks.update.$1", - "type": "string", - "tags": [], - "label": "id", - "description": [], - "signature": [ - "string" - ], - "source": { - "path": "src/core/public/chrome/nav_links/nav_links_service.ts", - "lineNumber": 71 - }, - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "core", - "id": "def-public.ChromeNavLinks.update.$2", - "type": "Object", - "tags": [], - "label": "values", - "description": [], - "signature": [ - "Partial>" - ], - "source": { - "path": "src/core/public/chrome/nav_links/nav_links_service.ts", - "lineNumber": 71 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [] - }, { "parentPluginId": "core", "id": "def-public.ChromeNavLinks.enableForcedAppSwitcherNavigation", @@ -1408,7 +1311,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_links_service.ts", - "lineNumber": 85 + "lineNumber": 73 }, "deprecated": false, "children": [], @@ -1430,7 +1333,7 @@ ], "source": { "path": "src/core/public/chrome/nav_links/nav_links_service.ts", - "lineNumber": 90 + "lineNumber": 78 }, "deprecated": false, "children": [], @@ -2678,23 +2581,6 @@ "deprecated": false, "initialIsOpen": false }, - { - "parentPluginId": "core", - "id": "def-public.ChromeNavLinkUpdateableFields", - "type": "Type", - "tags": [], - "label": "ChromeNavLinkUpdateableFields", - "description": [], - "signature": [ - "{ readonly hidden?: boolean | undefined; readonly url?: string | undefined; readonly disabled?: boolean | undefined; readonly href?: string | undefined; }" - ], - "source": { - "path": "src/core/public/chrome/nav_links/nav_link.ts", - "lineNumber": 85 - }, - "deprecated": false, - "initialIsOpen": false - }, { "parentPluginId": "core", "id": "def-public.NavType", diff --git a/api_docs/data.json b/api_docs/data.json index 77d26811db5bf4..ab5196934d855b 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -11486,111 +11486,6 @@ "lineNumber": 58 } }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 10 - } - }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 33 - } - }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 51 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 42 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 9 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 43 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", - "lineNumber": 12 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", - "lineNumber": 9 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", - "lineNumber": 11 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", - "lineNumber": 11 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", - "lineNumber": 23 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", - "lineNumber": 45 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", - "lineNumber": 148 - } - }, { "plugin": "maps", "link": { @@ -11955,6 +11850,90 @@ "lineNumber": 66 } }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 10 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 42 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 9 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 43 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", + "lineNumber": 10 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", + "lineNumber": 12 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", + "lineNumber": 9 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", + "lineNumber": 11 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", + "lineNumber": 11 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", + "lineNumber": 23 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", + "lineNumber": 45 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", + "lineNumber": 148 + } + }, { "plugin": "ml", "link": { @@ -12207,6 +12186,27 @@ "lineNumber": 21 } }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 10 + } + }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 33 + } + }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 51 + } + }, { "plugin": "fleet", "link": { @@ -12747,409 +12747,346 @@ } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 19 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 10 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 30 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 16 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 54 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 29 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 84 + "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", + "lineNumber": 12 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 150 + "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", + "lineNumber": 16 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 18 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 26 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 30 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 28 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 33 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 57 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 35 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 95 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", + "lineNumber": 8 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 96 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", + "lineNumber": 57 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 101 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 114 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 139 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 121 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 248 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 125 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 287 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 125 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 329 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 130 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 19 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 131 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 30 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 133 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 54 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 134 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 84 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 150 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", - "lineNumber": 27 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 26 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 16 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 28 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 32 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 57 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 19 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 95 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 36 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 96 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 101 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 23 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 114 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 121 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", - "lineNumber": 29 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 125 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 31 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 125 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 91 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 130 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 19 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 131 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 333 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 133 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 339 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 134 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 347 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 355 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", + "lineNumber": 27 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 363 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 13 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 371 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 16 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 380 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 32 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 62 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 19 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 166 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 36 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 167 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 13 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 629 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 23 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 666 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", + "lineNumber": 14 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 691 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 8 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 57 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", - "lineNumber": 10 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", - "lineNumber": 16 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", "lineNumber": 29 } }, { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", - "lineNumber": 12 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", - "lineNumber": 16 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 18 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 30 - } - }, - { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 33 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 31 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 35 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 91 } }, { @@ -13391,192 +13328,213 @@ } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx", - "lineNumber": 12 + "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", + "lineNumber": 1 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx", - "lineNumber": 20 - } + "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", + "lineNumber": 22 + } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 3 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 22 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 7 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 8 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 9 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", + "lineNumber": 19 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 333 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 339 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 347 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 355 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 363 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 371 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 380 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 62 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx", - "lineNumber": 17 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 166 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 43 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 167 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 104 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 629 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 666 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 139 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 691 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 248 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 11 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 287 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 333 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 329 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 339 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", - "lineNumber": 1 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 347 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", - "lineNumber": 22 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 355 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 3 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 363 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 7 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 371 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 8 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 380 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 9 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts", + "lineNumber": 1 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts", + "lineNumber": 2 } }, { @@ -14103,48 +14061,6 @@ }, "deprecated": true, "references": [ - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 34 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 78 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 86 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 88 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", - "lineNumber": 8 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", - "lineNumber": 86 - } - }, { "plugin": "savedObjects", "link": { @@ -14237,206 +14153,178 @@ } }, { - "plugin": "savedObjectsManagement", + "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", - "lineNumber": 37 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 10 } }, { - "plugin": "savedObjectsManagement", + "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", - "lineNumber": 89 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 29 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", - "lineNumber": 9 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 29 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", - "lineNumber": 24 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 42 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", - "lineNumber": 15 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 46 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", - "lineNumber": 28 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 9 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", - "lineNumber": 14 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 32 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", - "lineNumber": 85 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 32 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", - "lineNumber": 47 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 43 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", - "lineNumber": 172 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 50 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", - "lineNumber": 42 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", - "lineNumber": 96 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", - "lineNumber": 166 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 29 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", + "lineNumber": 22 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 29 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", + "lineNumber": 171 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 42 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", + "lineNumber": 15 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 46 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", + "lineNumber": 18 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 9 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", + "lineNumber": 12 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 32 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", + "lineNumber": 15 } }, { - "plugin": "indexPatternManagement", + "plugin": "savedObjectsManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 32 + "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", + "lineNumber": 37 } }, { - "plugin": "indexPatternManagement", + "plugin": "savedObjectsManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 43 + "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", + "lineNumber": 89 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 50 + "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", + "lineNumber": 14 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", - "lineNumber": 22 + "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", + "lineNumber": 85 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", - "lineNumber": 171 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", + "lineNumber": 47 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", - "lineNumber": 15 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", + "lineNumber": 172 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", - "lineNumber": 18 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", + "lineNumber": 42 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", - "lineNumber": 12 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", + "lineNumber": 96 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", - "lineNumber": 15 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", + "lineNumber": 166 } }, { @@ -14509,6 +14397,48 @@ "lineNumber": 30 } }, + { + "plugin": "apm", + "link": { + "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", + "lineNumber": 14 + } + }, + { + "plugin": "apm", + "link": { + "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", + "lineNumber": 31 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", + "lineNumber": 9 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", + "lineNumber": 24 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", + "lineNumber": 15 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", + "lineNumber": 28 + } + }, { "plugin": "infra", "link": { @@ -14649,32 +14579,74 @@ "lineNumber": 23 } }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 34 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 78 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 86 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 88 + } + }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts", - "lineNumber": 20 + "lineNumber": 21 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts", - "lineNumber": 64 + "lineNumber": 66 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts", - "lineNumber": 17 + "lineNumber": 19 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts", - "lineNumber": 98 + "lineNumber": 100 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", + "lineNumber": 8 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", + "lineNumber": 86 } }, { @@ -14912,21 +14884,21 @@ "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 32 + "lineNumber": 40 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 38 + "lineNumber": 55 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 43 + "lineNumber": 60 } }, { @@ -15013,34 +14985,6 @@ "lineNumber": 33 } }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx", - "lineNumber": 9 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx", - "lineNumber": 21 - } - }, - { - "plugin": "apm", - "link": { - "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", - "lineNumber": 14 - } - }, - { - "plugin": "apm", - "link": { - "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", - "lineNumber": 31 - } - }, { "plugin": "uptime", "link": { @@ -15083,6 +15027,20 @@ "lineNumber": 36 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts", + "lineNumber": 1 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts", + "lineNumber": 66 + } + }, { "plugin": "infra", "link": { @@ -15126,486 +15084,444 @@ } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", - "lineNumber": 2 + "path": "x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts", + "lineNumber": 1 } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", - "lineNumber": 7 + "path": "x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts", + "lineNumber": 10 } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", - "lineNumber": 1 - } - }, - { - "plugin": "infra", - "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", - "lineNumber": 4 + "path": "x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts", + "lineNumber": 23 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", - "lineNumber": 1 + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", + "lineNumber": 2 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", - "lineNumber": 5 + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", + "lineNumber": 7 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", "lineNumber": 1 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", - "lineNumber": 11 + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", + "lineNumber": 4 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", "lineNumber": 1 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", - "lineNumber": 4 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", + "lineNumber": 5 } }, { - "plugin": "ml", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", - "lineNumber": 2 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts", + "lineNumber": 3 } }, { - "plugin": "ml", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", - "lineNumber": 3 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts", + "lineNumber": 25 } }, { - "plugin": "ml", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", - "lineNumber": 4 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts", + "lineNumber": 55 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 21 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts", + "lineNumber": 20 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 100 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts", + "lineNumber": 21 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 121 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts", + "lineNumber": 3 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 13 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts", + "lineNumber": 9 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 17 + "path": "x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts", + "lineNumber": 2 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 29 + "path": "x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts", + "lineNumber": 40 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 19 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts", + "lineNumber": 2 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 38 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts", + "lineNumber": 10 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "path": "x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts", "lineNumber": 25 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 31 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 53 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 58 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 61 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", + "lineNumber": 4 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 22 + "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", + "lineNumber": 2 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 50 + "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", + "lineNumber": 3 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 55 + "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", + "lineNumber": 4 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 58 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", + "lineNumber": 12 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 62 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", + "lineNumber": 412 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 96 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", + "lineNumber": 12 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 169 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", + "lineNumber": 60 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 169 + "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", + "lineNumber": 8 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 184 + "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", + "lineNumber": 37 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 187 + "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", + "lineNumber": 8 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 196 + "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 199 + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", + "lineNumber": 25 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 208 + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", + "lineNumber": 28 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 214 + "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", + "lineNumber": 14 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 226 + "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", + "lineNumber": 193 } }, { - "plugin": "lists", + "plugin": "visTypeTimeseries", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 229 + "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "visTypeTimeseries", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 239 + "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", + "lineNumber": 20 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 267 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 285 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 21 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 320 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 100 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 336 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 121 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1026 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 13 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1045 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 17 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1095 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 29 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1251 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 19 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1302 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 38 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1352 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 13 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", - "lineNumber": 15 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 25 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", "lineNumber": 31 } }, { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", - "lineNumber": 12 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", - "lineNumber": 412 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", - "lineNumber": 12 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", - "lineNumber": 60 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", - "lineNumber": 8 - } - }, - { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", - "lineNumber": 37 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 53 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", - "lineNumber": 8 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 58 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 61 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 25 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 22 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 28 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 50 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", - "lineNumber": 14 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 55 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", - "lineNumber": 193 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 58 } }, { @@ -15629,6 +15545,20 @@ "lineNumber": 53 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", + "lineNumber": 16 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", + "lineNumber": 31 + } + }, { "plugin": "securitySolution", "link": { @@ -15756,164 +15686,220 @@ } }, { - "plugin": "securitySolution", + "plugin": "stackAlerts", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", + "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", "lineNumber": 13 } }, { - "plugin": "securitySolution", + "plugin": "stackAlerts", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 24 + "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", + "lineNumber": 25 } }, { - "plugin": "securitySolution", + "plugin": "infra", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 43 + "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", + "lineNumber": 13 } }, { - "plugin": "securitySolution", + "plugin": "infra", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 49 + "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", + "lineNumber": 30 } }, { - "plugin": "securitySolution", + "plugin": "infra", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 368 + "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", + "lineNumber": 1 } }, { - "plugin": "visTypeTimeseries", + "plugin": "infra", "link": { - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", - "lineNumber": 11 + "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", + "lineNumber": 3 } }, { - "plugin": "visTypeTimeseries", + "plugin": "lists", "link": { - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", - "lineNumber": 20 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 62 } }, { - "plugin": "stackAlerts", + "plugin": "lists", "link": { - "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 96 } }, { - "plugin": "stackAlerts", + "plugin": "lists", "link": { - "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", - "lineNumber": 25 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 169 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 169 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", - "lineNumber": 30 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 184 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", - "lineNumber": 1 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 187 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", - "lineNumber": 3 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 196 } }, { - "plugin": "discover", + "plugin": "lists", "link": { - "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", - "lineNumber": 17 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 199 } }, { - "plugin": "discover", + "plugin": "lists", "link": { - "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", - "lineNumber": 26 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 208 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", - "lineNumber": 18 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 214 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", - "lineNumber": 95 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 226 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 10 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 229 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 53 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 239 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 61 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 267 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 69 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 285 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", - "lineNumber": 19 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 320 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", - "lineNumber": 24 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 336 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1026 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1045 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1095 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1251 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1302 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1352 + } + }, + { + "plugin": "discover", + "link": { + "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", + "lineNumber": 17 + } + }, + { + "plugin": "discover", + "link": { + "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", + "lineNumber": 26 } }, { @@ -15980,52 +15966,108 @@ } }, { - "plugin": "ml", + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx", + "lineNumber": 57 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", + "lineNumber": 14 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", + "lineNumber": 27 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", + "lineNumber": 213 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", + "lineNumber": 234 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", + "lineNumber": 12 + } + }, + { + "plugin": "ml", + "link": { + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", + "lineNumber": 17 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", + "lineNumber": 18 + } + }, + { + "plugin": "observability", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/tabs/custom_urls.tsx", - "lineNumber": 57 + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", + "lineNumber": 95 } }, { - "plugin": "ml", + "plugin": "observability", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 14 + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 10 } }, { - "plugin": "ml", + "plugin": "observability", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 27 + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 53 } }, { - "plugin": "ml", + "plugin": "observability", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 213 + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 61 } }, { - "plugin": "ml", + "plugin": "observability", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/lens_utils.ts", - "lineNumber": 234 + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 69 } }, { - "plugin": "ml", + "plugin": "observability", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", - "lineNumber": 12 + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", + "lineNumber": 19 } }, { - "plugin": "ml", + "plugin": "observability", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_row/action_menu/actions.ts", - "lineNumber": 17 + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", + "lineNumber": 24 } }, { @@ -16098,20 +16140,6 @@ "lineNumber": 19 } }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", - "lineNumber": 8 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", - "lineNumber": 10 - } - }, { "plugin": "stackAlerts", "link": { @@ -16231,6 +16259,20 @@ "lineNumber": 13 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", + "lineNumber": 8 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", + "lineNumber": 10 + } + }, { "plugin": "ml", "link": { @@ -33252,111 +33294,6 @@ "lineNumber": 58 } }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 10 - } - }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 33 - } - }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 51 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 42 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 9 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 43 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", - "lineNumber": 12 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", - "lineNumber": 9 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", - "lineNumber": 11 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", - "lineNumber": 11 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", - "lineNumber": 23 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", - "lineNumber": 45 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", - "lineNumber": 148 - } - }, { "plugin": "maps", "link": { @@ -33701,24 +33638,108 @@ } }, { - "plugin": "maps", + "plugin": "maps", + "link": { + "path": "x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx", + "lineNumber": 59 + } + }, + { + "plugin": "maps", + "link": { + "path": "x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx", + "lineNumber": 60 + } + }, + { + "plugin": "maps", + "link": { + "path": "x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx", + "lineNumber": 66 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 10 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 42 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 9 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 43 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", + "lineNumber": 10 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", + "lineNumber": 12 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", + "lineNumber": 9 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", + "lineNumber": 11 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", + "lineNumber": 11 + } + }, + { + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx", - "lineNumber": 59 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", + "lineNumber": 23 } }, { - "plugin": "maps", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx", - "lineNumber": 60 + "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", + "lineNumber": 45 } }, { - "plugin": "maps", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx", - "lineNumber": 66 + "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", + "lineNumber": 148 } }, { @@ -33973,6 +33994,27 @@ "lineNumber": 21 } }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 10 + } + }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 33 + } + }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 51 + } + }, { "plugin": "fleet", "link": { @@ -34513,409 +34555,346 @@ } }, { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 19 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 30 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 54 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 84 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 150 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 11 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 26 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 28 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 57 - } - }, - { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 95 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 10 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 96 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 16 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 101 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 29 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 114 + "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", + "lineNumber": 12 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 121 + "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", + "lineNumber": 16 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 125 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 18 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 125 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 30 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 130 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 33 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 131 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 35 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 133 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", + "lineNumber": 8 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 134 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", + "lineNumber": 57 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", - "lineNumber": 27 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 139 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 13 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 248 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 16 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 287 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 32 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 329 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", "lineNumber": 19 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 36 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 30 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 54 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 23 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 84 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 150 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", - "lineNumber": 29 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 31 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 26 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 91 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 28 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 19 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 57 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 333 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 95 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 339 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 96 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 347 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 101 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 355 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 114 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 363 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 121 } }, { "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 371 + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 125 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 380 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 125 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 62 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 130 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 166 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 131 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 167 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 133 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 629 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 134 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 666 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 691 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", + "lineNumber": 27 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 8 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 13 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 57 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 16 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", - "lineNumber": 10 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 32 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", - "lineNumber": 16 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 19 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", - "lineNumber": 29 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 36 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", - "lineNumber": 12 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 13 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", - "lineNumber": 16 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 23 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 18 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", + "lineNumber": 14 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 30 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", + "lineNumber": 29 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 33 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 31 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 35 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 91 } }, { @@ -35157,192 +35136,213 @@ } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx", - "lineNumber": 12 + "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", + "lineNumber": 1 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx", - "lineNumber": 20 + "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", + "lineNumber": 22 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 3 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 22 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 7 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 8 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 9 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", + "lineNumber": 19 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 333 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 339 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 347 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 355 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 363 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 371 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 380 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 62 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx", - "lineNumber": 17 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 166 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 43 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 167 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 104 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 629 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 666 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 139 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 691 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 248 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 11 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 287 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 333 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 329 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 339 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", - "lineNumber": 1 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 347 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", - "lineNumber": 22 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 355 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 3 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 363 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 7 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 371 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 8 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 380 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 9 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts", + "lineNumber": 1 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts", + "lineNumber": 2 } }, { diff --git a/api_docs/data_index_patterns.json b/api_docs/data_index_patterns.json index 08c47437faa56c..6d9230cfb6a877 100644 --- a/api_docs/data_index_patterns.json +++ b/api_docs/data_index_patterns.json @@ -5513,111 +5513,6 @@ "lineNumber": 58 } }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 10 - } - }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 33 - } - }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", - "lineNumber": 51 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 42 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 9 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 43 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", - "lineNumber": 12 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", - "lineNumber": 9 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", - "lineNumber": 11 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", - "lineNumber": 11 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", - "lineNumber": 23 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", - "lineNumber": 45 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", - "lineNumber": 148 - } - }, { "plugin": "maps", "link": { @@ -5982,6 +5877,90 @@ "lineNumber": 66 } }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 10 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 42 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 9 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 43 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", + "lineNumber": 10 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/lib/extract_time_fields.ts", + "lineNumber": 12 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", + "lineNumber": 9 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/types.ts", + "lineNumber": 11 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", + "lineNumber": 11 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/indexed_fields_table.tsx", + "lineNumber": 23 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", + "lineNumber": 45 + } + }, + { + "plugin": "indexPatternManagement", + "link": { + "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx", + "lineNumber": 148 + } + }, { "plugin": "ml", "link": { @@ -6234,6 +6213,27 @@ "lineNumber": 21 } }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 10 + } + }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 33 + } + }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/public/applications/fleet/components/search_bar.tsx", + "lineNumber": 51 + } + }, { "plugin": "fleet", "link": { @@ -6774,409 +6774,346 @@ } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 19 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 10 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 30 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 16 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 54 + "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "lineNumber": 29 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 84 + "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", + "lineNumber": 12 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", - "lineNumber": 150 + "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", + "lineNumber": 16 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 18 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 26 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 30 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 28 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 33 } }, { - "plugin": "lists", + "plugin": "maps", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 57 + "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", + "lineNumber": 35 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 95 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", + "lineNumber": 8 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 96 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", + "lineNumber": 57 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 101 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 114 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 139 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 121 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 248 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 125 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 287 } }, { - "plugin": "lists", + "plugin": "lens", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 125 + "path": "x-pack/plugins/lens/server/routes/field_stats.ts", + "lineNumber": 329 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 130 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 19 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 131 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 30 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 133 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 54 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 134 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 84 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.ts", + "lineNumber": 150 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", - "lineNumber": 27 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 26 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 16 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 28 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 32 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 57 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 19 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 95 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 36 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 96 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 101 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 23 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 114 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 121 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", - "lineNumber": 29 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 125 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 31 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 125 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 91 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 130 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 19 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 131 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 333 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 133 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 339 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 134 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 347 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 355 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/operator.tsx", + "lineNumber": 27 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 363 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 13 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 371 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 16 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", - "lineNumber": 380 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 32 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 62 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 19 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 166 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 36 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 167 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 13 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 629 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 23 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 666 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", + "lineNumber": 14 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 691 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 8 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/types.ts", - "lineNumber": 57 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", - "lineNumber": 10 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", - "lineNumber": 16 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx", "lineNumber": 29 } }, { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", - "lineNumber": 12 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/components/geo_field_select.tsx", - "lineNumber": 16 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 18 - } - }, - { - "plugin": "maps", - "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 30 - } - }, - { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 33 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 31 } }, { - "plugin": "maps", + "plugin": "lists", "link": { - "path": "x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx", - "lineNumber": 35 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 91 } }, { @@ -7418,192 +7355,213 @@ } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx", - "lineNumber": 12 + "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", + "lineNumber": 1 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx", - "lineNumber": 20 - } + "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", + "lineNumber": 22 + } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 3 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 22 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 7 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 8 } }, { - "plugin": "securitySolution", + "plugin": "lens", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", + "lineNumber": 9 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", + "lineNumber": 19 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 333 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 339 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 347 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 355 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 363 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 371 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/helpers.test.ts", "lineNumber": 380 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 62 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx", - "lineNumber": 17 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 166 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 43 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 167 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 104 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 629 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 666 } }, { - "plugin": "lens", + "plugin": "lists", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 139 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 691 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 248 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 11 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 287 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 333 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/server/routes/field_stats.ts", - "lineNumber": 329 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 339 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", - "lineNumber": 1 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 347 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/public/indexpattern_datasource/types.d.ts", - "lineNumber": 22 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 355 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 3 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 363 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 7 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 371 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 8 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts", + "lineNumber": 380 } }, { - "plugin": "lens", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lens/target/types/server/routes/field_stats.d.ts", - "lineNumber": 9 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts", + "lineNumber": 1 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts", + "lineNumber": 2 } }, { @@ -8130,48 +8088,6 @@ }, "deprecated": true, "references": [ - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 34 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 78 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 86 - } - }, - { - "plugin": "lists", - "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", - "lineNumber": 88 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", - "lineNumber": 8 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", - "lineNumber": 86 - } - }, { "plugin": "savedObjects", "link": { @@ -8264,206 +8180,178 @@ } }, { - "plugin": "savedObjectsManagement", + "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", - "lineNumber": 37 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 10 } }, { - "plugin": "savedObjectsManagement", + "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", - "lineNumber": 89 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 29 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", - "lineNumber": 9 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 29 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", - "lineNumber": 24 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 42 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", - "lineNumber": 15 + "path": "src/plugins/index_pattern_management/public/service/list/config.ts", + "lineNumber": 46 } }, { - "plugin": "infra", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", - "lineNumber": 28 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 9 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", - "lineNumber": 14 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 32 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", - "lineNumber": 85 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 32 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", - "lineNumber": 47 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 43 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", - "lineNumber": 172 + "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", + "lineNumber": 50 } }, { - "plugin": "lens", + "plugin": "indexPatternManagement", "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", - "lineNumber": 42 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", - "lineNumber": 96 - } - }, - { - "plugin": "lens", - "link": { - "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", - "lineNumber": 166 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 10 - } - }, - { - "plugin": "indexPatternManagement", - "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 29 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", + "lineNumber": 22 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 29 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", + "lineNumber": 171 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 42 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", + "lineNumber": 15 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/config.ts", - "lineNumber": 46 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", + "lineNumber": 18 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 9 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", + "lineNumber": 12 } }, { "plugin": "indexPatternManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 32 + "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", + "lineNumber": 15 } }, { - "plugin": "indexPatternManagement", + "plugin": "savedObjectsManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 32 + "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", + "lineNumber": 37 } }, { - "plugin": "indexPatternManagement", + "plugin": "savedObjectsManagement", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 43 + "path": "src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx", + "lineNumber": 89 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/service/list/manager.ts", - "lineNumber": 50 + "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", + "lineNumber": 14 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", - "lineNumber": 22 + "path": "x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx", + "lineNumber": 85 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/indexed_fields_table/components/table/table.tsx", - "lineNumber": 171 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", + "lineNumber": 47 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", - "lineNumber": 15 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx", + "lineNumber": 172 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx", - "lineNumber": 18 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", + "lineNumber": 42 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", - "lineNumber": 12 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", + "lineNumber": 96 } }, { - "plugin": "indexPatternManagement", + "plugin": "lens", "link": { - "path": "src/plugins/index_pattern_management/public/components/edit_index_pattern/index_header/index_header.tsx", - "lineNumber": 15 + "path": "x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx", + "lineNumber": 166 } }, { @@ -8536,6 +8424,48 @@ "lineNumber": 30 } }, + { + "plugin": "apm", + "link": { + "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", + "lineNumber": 14 + } + }, + { + "plugin": "apm", + "link": { + "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", + "lineNumber": 31 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", + "lineNumber": 9 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx", + "lineNumber": 24 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", + "lineNumber": 15 + } + }, + { + "plugin": "infra", + "link": { + "path": "x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx", + "lineNumber": 28 + } + }, { "plugin": "infra", "link": { @@ -8676,32 +8606,74 @@ "lineNumber": 23 } }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 34 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 78 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 86 + } + }, + { + "plugin": "lists", + "link": { + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_items_renderer.tsx", + "lineNumber": 88 + } + }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts", - "lineNumber": 20 + "lineNumber": 21 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts", - "lineNumber": 64 + "lineNumber": 66 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts", - "lineNumber": 17 + "lineNumber": 19 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts", - "lineNumber": 98 + "lineNumber": 100 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", + "lineNumber": 8 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", + "lineNumber": 86 } }, { @@ -8939,21 +8911,21 @@ "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 32 + "lineNumber": 40 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 38 + "lineNumber": 55 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts", - "lineNumber": 43 + "lineNumber": 60 } }, { @@ -9040,34 +9012,6 @@ "lineNumber": 33 } }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx", - "lineNumber": 9 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx", - "lineNumber": 21 - } - }, - { - "plugin": "apm", - "link": { - "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", - "lineNumber": 14 - } - }, - { - "plugin": "apm", - "link": { - "path": "x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx", - "lineNumber": 31 - } - }, { "plugin": "uptime", "link": { @@ -9110,6 +9054,20 @@ "lineNumber": 36 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts", + "lineNumber": 1 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts", + "lineNumber": 66 + } + }, { "plugin": "infra", "link": { @@ -9153,507 +9111,479 @@ } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", - "lineNumber": 2 + "path": "x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts", + "lineNumber": 1 } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", - "lineNumber": 7 + "path": "x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts", + "lineNumber": 10 } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", - "lineNumber": 1 - } - }, - { - "plugin": "infra", - "link": { - "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", - "lineNumber": 4 + "path": "x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts", + "lineNumber": 23 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", - "lineNumber": 1 + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", + "lineNumber": 2 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", - "lineNumber": 5 + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/components/expression_chart.d.ts", + "lineNumber": 7 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", "lineNumber": 1 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", - "lineNumber": 11 + "path": "x-pack/plugins/infra/target/types/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.d.ts", + "lineNumber": 4 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", "lineNumber": 1 } }, { "plugin": "infra", "link": { - "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", - "lineNumber": 4 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/index.d.ts", + "lineNumber": 5 } }, { - "plugin": "ml", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", - "lineNumber": 2 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts", + "lineNumber": 3 } }, { - "plugin": "ml", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", - "lineNumber": 3 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts", + "lineNumber": 25 } }, { - "plugin": "ml", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", - "lineNumber": 4 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts", + "lineNumber": 55 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 11 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 21 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts", + "lineNumber": 20 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 100 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts", + "lineNumber": 21 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", - "lineNumber": 121 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts", + "lineNumber": 3 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 13 + "path": "x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts", + "lineNumber": 9 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 17 + "path": "x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts", + "lineNumber": 2 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 29 + "path": "x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts", + "lineNumber": 40 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 19 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts", + "lineNumber": 2 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", - "lineNumber": 38 + "path": "x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts", + "lineNumber": 10 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "path": "x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts", "lineNumber": 25 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 31 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 53 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.d.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 58 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", + "lineNumber": 1 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", - "lineNumber": 61 + "path": "x-pack/plugins/infra/target/types/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.d.ts", + "lineNumber": 4 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 22 + "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", + "lineNumber": 2 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 50 + "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", + "lineNumber": 3 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 55 + "path": "x-pack/plugins/ml/target/types/public/application/jobs/new_job/common/index_pattern_context.d.ts", + "lineNumber": 4 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", - "lineNumber": 58 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", + "lineNumber": 12 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 62 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", + "lineNumber": 412 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 96 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", + "lineNumber": 12 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 169 + "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", + "lineNumber": 60 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 169 + "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", + "lineNumber": 8 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 184 + "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", + "lineNumber": 37 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 187 + "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", + "lineNumber": 8 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 196 + "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 199 + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", + "lineNumber": 25 } }, { - "plugin": "lists", + "plugin": "ml", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 208 + "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", + "lineNumber": 28 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 214 + "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", + "lineNumber": 14 } }, { - "plugin": "lists", + "plugin": "infra", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 226 + "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", + "lineNumber": 193 } }, { - "plugin": "lists", + "plugin": "visTypeTimeseries", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 229 + "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", + "lineNumber": 11 } }, { - "plugin": "lists", + "plugin": "visTypeTimeseries", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 239 + "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", + "lineNumber": 20 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 267 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 11 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 285 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 21 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 320 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 100 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 336 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field.tsx", + "lineNumber": 121 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1026 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 13 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1045 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 17 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1095 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 29 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1251 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 19 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1302 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx", + "lineNumber": 38 } }, { "plugin": "lists", "link": { - "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", - "lineNumber": 1352 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 13 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", - "lineNumber": 15 + "path": "x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx", + "lineNumber": 25 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", "lineNumber": 31 } }, { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", - "lineNumber": 12 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx", - "lineNumber": 412 - } - }, - { - "plugin": "ml", - "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", - "lineNumber": 12 - } - }, - { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_delete/use_delete_action.tsx", - "lineNumber": 60 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 53 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", - "lineNumber": 8 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 58 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/utils.d.ts", - "lineNumber": 37 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx", + "lineNumber": 61 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", - "lineNumber": 8 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 22 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.d.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 50 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 25 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 55 } }, { - "plugin": "ml", + "plugin": "lists", "link": { - "path": "x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx", - "lineNumber": 28 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/exception_item_renderer.tsx", + "lineNumber": 58 } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", - "lineNumber": 14 + "path": "x-pack/plugins/security_solution/public/network/pages/navigation/types.ts", + "lineNumber": 9 } }, { - "plugin": "infra", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx", - "lineNumber": 193 + "path": "x-pack/plugins/security_solution/public/network/pages/navigation/types.ts", + "lineNumber": 35 } }, { "plugin": "securitySolution", "link": { "path": "x-pack/plugins/security_solution/public/network/pages/navigation/types.ts", - "lineNumber": 9 + "lineNumber": 53 } }, { "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security_solution/public/network/pages/navigation/types.ts", - "lineNumber": 35 + "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", + "lineNumber": 16 } }, { "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security_solution/public/network/pages/navigation/types.ts", - "lineNumber": 53 + "path": "x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts", + "lineNumber": 31 } }, { @@ -9736,211 +9666,267 @@ { "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx", - "lineNumber": 93 + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx", + "lineNumber": 93 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx", + "lineNumber": 114 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 12 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 19 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts", + "lineNumber": 31 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx", + "lineNumber": 19 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx", + "lineNumber": 30 + } + }, + { + "plugin": "stackAlerts", + "link": { + "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", + "lineNumber": 13 + } + }, + { + "plugin": "stackAlerts", + "link": { + "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", + "lineNumber": 25 } }, { - "plugin": "securitySolution", + "plugin": "infra", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field.tsx", - "lineNumber": 114 + "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", + "lineNumber": 13 } }, { - "plugin": "securitySolution", + "plugin": "infra", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 12 + "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", + "lineNumber": 30 } }, { - "plugin": "securitySolution", + "plugin": "infra", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 19 + "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", + "lineNumber": 1 } }, { - "plugin": "securitySolution", + "plugin": "infra", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts", - "lineNumber": 31 + "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", + "lineNumber": 3 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx", - "lineNumber": 19 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 62 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx", - "lineNumber": 30 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 96 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 169 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx", - "lineNumber": 24 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 169 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 43 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 184 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 49 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 187 } }, { - "plugin": "securitySolution", + "plugin": "lists", "link": { - "path": "x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx", - "lineNumber": 368 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 196 } }, { - "plugin": "visTypeTimeseries", + "plugin": "lists", "link": { - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", - "lineNumber": 11 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 199 } }, { - "plugin": "visTypeTimeseries", + "plugin": "lists", "link": { - "path": "src/plugins/vis_type_timeseries/common/index_patterns_utils.ts", - "lineNumber": 20 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 208 } }, { - "plugin": "stackAlerts", + "plugin": "lists", "link": { - "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 214 } }, { - "plugin": "stackAlerts", + "plugin": "lists", "link": { - "path": "x-pack/plugins/stack_alerts/server/alert_types/geo_containment/es_query_builder.ts", - "lineNumber": 25 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 226 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", - "lineNumber": 13 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 229 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts", - "lineNumber": 30 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 239 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", - "lineNumber": 1 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 267 } }, { - "plugin": "infra", + "plugin": "lists", "link": { - "path": "x-pack/plugins/infra/target/types/common/dependency_mocks/index_patterns.d.ts", - "lineNumber": 3 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 285 } }, { - "plugin": "discover", + "plugin": "lists", "link": { - "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", - "lineNumber": 17 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 320 } }, { - "plugin": "discover", + "plugin": "lists", "link": { - "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", - "lineNumber": 26 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 336 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", - "lineNumber": 18 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1026 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", - "lineNumber": 95 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1045 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 10 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1095 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 53 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1251 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 61 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1302 } }, { - "plugin": "observability", + "plugin": "lists", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", - "lineNumber": 69 + "path": "x-pack/plugins/lists/public/exceptions/components/builder/helpers.test.ts", + "lineNumber": 1352 } }, { - "plugin": "observability", + "plugin": "discover", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", - "lineNumber": 19 + "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", + "lineNumber": 17 } }, { - "plugin": "observability", + "plugin": "discover", "link": { - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", - "lineNumber": 24 + "path": "src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx", + "lineNumber": 26 } }, { @@ -10055,6 +10041,62 @@ "lineNumber": 17 } }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", + "lineNumber": 18 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts", + "lineNumber": 95 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 10 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 53 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 61 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts", + "lineNumber": 69 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", + "lineNumber": 19 + } + }, + { + "plugin": "observability", + "link": { + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts", + "lineNumber": 24 + } + }, { "plugin": "securitySolution", "link": { @@ -10125,20 +10167,6 @@ "lineNumber": 19 } }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", - "lineNumber": 8 - } - }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", - "lineNumber": 10 - } - }, { "plugin": "stackAlerts", "link": { @@ -10258,6 +10286,20 @@ "lineNumber": 13 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", + "lineNumber": 8 + } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/public/common/mock/index_pattern.ts", + "lineNumber": 10 + } + }, { "plugin": "ml", "link": { diff --git a/api_docs/data_search.json b/api_docs/data_search.json index a82a2ca24d899e..082553e94dcf42 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -2690,7 +2690,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 89 + "lineNumber": 90 }, "deprecated": false, "children": [ @@ -2708,7 +2708,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 90 + "lineNumber": 91 }, "deprecated": false, "returnComment": [], @@ -2758,7 +2758,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 91 + "lineNumber": 92 }, "deprecated": false, "returnComment": [], @@ -2806,7 +2806,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 92 + "lineNumber": 93 }, "deprecated": false, "returnComment": [], @@ -2875,7 +2875,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 93 + "lineNumber": 94 }, "deprecated": false, "returnComment": [], @@ -2923,7 +2923,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 94 + "lineNumber": 95 }, "deprecated": false, "returnComment": [], @@ -2955,7 +2955,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 95 + "lineNumber": 96 }, "deprecated": false, "returnComment": [], @@ -2995,7 +2995,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 96 + "lineNumber": 97 }, "deprecated": false, "returnComment": [], @@ -3128,7 +3128,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 42 + "lineNumber": 43 }, "deprecated": false, "children": [ @@ -3144,7 +3144,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 43 + "lineNumber": 44 }, "deprecated": false }, @@ -3202,7 +3202,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 48 + "lineNumber": 49 }, "deprecated": false, "returnComment": [], @@ -3216,7 +3216,7 @@ "description": [], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 52 + "lineNumber": 53 }, "deprecated": false }, @@ -3239,7 +3239,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 53 + "lineNumber": 54 }, "deprecated": false } @@ -3266,7 +3266,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 59 + "lineNumber": 60 }, "deprecated": false } @@ -3292,7 +3292,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 99 + "lineNumber": 100 }, "deprecated": false, "children": [ @@ -3308,7 +3308,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 103 + "lineNumber": 104 }, "deprecated": false }, @@ -3334,7 +3334,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 109 + "lineNumber": 110 }, "deprecated": false, "returnComment": [], @@ -3351,7 +3351,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 110 + "lineNumber": 111 }, "deprecated": false } @@ -3384,7 +3384,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 112 + "lineNumber": 113 }, "deprecated": false, "returnComment": [], @@ -3408,7 +3408,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 112 + "lineNumber": 113 }, "deprecated": false } @@ -3442,7 +3442,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 113 + "lineNumber": 114 }, "deprecated": false } @@ -3470,7 +3470,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 71 + "lineNumber": 72 }, "deprecated": false, "children": [ @@ -3504,7 +3504,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 75 + "lineNumber": 76 }, "deprecated": false, "returnComment": [], @@ -3521,7 +3521,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 76 + "lineNumber": 77 }, "deprecated": false }, @@ -3543,7 +3543,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 77 + "lineNumber": 78 }, "deprecated": false }, @@ -3565,7 +3565,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 78 + "lineNumber": 79 }, "deprecated": false } @@ -3599,7 +3599,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 80 + "lineNumber": 81 }, "deprecated": false }, @@ -3631,7 +3631,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 81 + "lineNumber": 82 }, "deprecated": false } @@ -4027,6 +4027,29 @@ "lineNumber": 39 }, "deprecated": false + }, + { + "parentPluginId": "data", + "id": "def-server.SearchStrategyDependencies.request", + "type": "Object", + "tags": [], + "label": "request", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "source": { + "path": "src/plugins/data/server/search/types.ts", + "lineNumber": 40 + }, + "deprecated": false } ], "initialIsOpen": false @@ -4122,7 +4145,7 @@ ], "source": { "path": "src/plugins/data/server/search/types.ts", - "lineNumber": 118 + "lineNumber": 119 }, "deprecated": false, "initialIsOpen": false diff --git a/api_docs/deprecations.mdx b/api_docs/deprecations.mdx index 7d6df4965c7120..9eee6d51d84ab9 100644 --- a/api_docs/deprecations.mdx +++ b/api_docs/deprecations.mdx @@ -42,15 +42,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex -## beatsManagement - -| Deprecated API | Reference location | Remove By | -| ---------------|-----------|-----------| -| | [kibana_database_adapter.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts#L8) | 7.16 | -| | [kibana_database_adapter.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts#L28) | 7.16 | - - - ## canvas | Deprecated API | Reference location | Remove By | @@ -783,7 +774,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [field_value_match.tsx#L36](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx#L36) | - | | | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx#L13) | - | | | [field_value_match_any.tsx#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx#L23) | - | -| | [field_value_lists.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L13) | - | +| | [field_value_lists.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L14) | - | | | [field_value_lists.tsx#L29](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L29) | - | | | [entry_renderer.tsx#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx#L31) | - | | | [entry_renderer.tsx#L91](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx#L91) | - | @@ -830,7 +821,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [field_value_match.tsx#L36](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx#L36) | - | | | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx#L13) | - | | | [field_value_match_any.tsx#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx#L23) | - | -| | [field_value_lists.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L13) | - | +| | [field_value_lists.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L14) | - | | | [field_value_lists.tsx#L29](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L29) | - | | | [entry_renderer.tsx#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx#L31) | - | | | [entry_renderer.tsx#L91](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx#L91) | - | @@ -923,7 +914,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [field_value_match.tsx#L36](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match.tsx#L36) | - | | | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx#L13) | - | | | [field_value_match_any.tsx#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_match_any.tsx#L23) | - | -| | [field_value_lists.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L13) | - | +| | [field_value_lists.tsx#L14](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L14) | - | | | [field_value_lists.tsx#L29](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/autocomplete/field_value_lists.tsx#L29) | - | | | [entry_renderer.tsx#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx#L31) | - | | | [entry_renderer.tsx#L91](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/public/exceptions/components/builder/entry_renderer.tsx#L91) | - | @@ -944,6 +935,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [get_call_cluster.mock.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts#L9) | 7.16 | | | [get_call_cluster.mock.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts#L27) | 7.16 | | | [get_call_cluster.mock.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/server/schemas/common/get_call_cluster.mock.ts#L31) | 7.16 | +| | [get_call_cluster.mock.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/target/types/server/schemas/common/get_call_cluster.mock.d.ts#L2) | 7.16 | +| | [get_call_cluster.mock.d.ts#L4](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/target/types/server/schemas/common/get_call_cluster.mock.d.ts#L4) | 7.16 | +| | [get_call_cluster.mock.d.ts#L5](https://github.com/elastic/kibana/tree/master/x-pack/plugins/lists/target/types/server/schemas/common/get_call_cluster.mock.d.ts#L5) | 7.16 | @@ -1579,12 +1573,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location | Remove By | | ---------------|-----------|-----------| +| | [types.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L21) | - | +| | [types.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L66) | - | +| | [action.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L19) | - | +| | [action.ts#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L100) | - | | | [index.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L8) | - | | | [index.ts#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L86) | - | -| | [types.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L20) | - | -| | [types.ts#L64](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L64) | - | -| | [action.ts#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L17) | - | -| | [action.ts#L98](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L98) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#L12) | - | | | [index.tsx#L48](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#L48) | - | | | [index.tsx#L122](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#L122) | - | @@ -1618,9 +1612,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L32](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L32) | - | -| | [middleware.ts#L38](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L38) | - | -| | [middleware.ts#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L43) | - | +| | [middleware.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L40) | - | +| | [middleware.ts#L55](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L55) | - | +| | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | @@ -1633,13 +1627,30 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.tsx#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx#L44) | - | | | [index.tsx#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx#L21) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx#L33) | - | -| | [index.tsx#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx#L9) | - | -| | [index.tsx#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx#L21) | - | -| | [get_query_filter.ts#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L15) | - | -| | [get_query_filter.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L31) | - | +| | [index.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts#L1) | - | +| | [index.d.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts#L66) | - | +| | [index.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts#L1) | - | +| | [index.d.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts#L10) | - | +| | [index.d.ts#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts#L23) | - | +| | [types.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts#L3) | - | +| | [types.d.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts#L25) | - | +| | [types.d.ts#L55](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts#L55) | - | +| | [columns.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts#L1) | - | +| | [columns.d.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts#L20) | - | +| | [columns.d.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts#L21) | - | +| | [index.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts#L3) | - | +| | [index.d.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts#L9) | - | +| | [types.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts#L2) | - | +| | [types.d.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts#L40) | - | +| | [index.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts#L2) | - | +| | [index.d.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts#L10) | - | +| | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts#L1) | - | +| | [types.d.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts#L25) | - | | | [types.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts#L9) | - | | | [types.ts#L35](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts#L35) | - | | | [types.ts#L53](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts#L53) | - | +| | [get_query_filter.ts#L16](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L16) | - | +| | [get_query_filter.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L31) | - | | | [helpers.tsx#L39](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx#L39) | - | | | [helpers.tsx#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx#L49) | - | | | [helpers.tsx#L52](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx#L52) | - | @@ -1658,11 +1669,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L31) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L30) | - | -| | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L13) | - | -| | [field_value_match_any.tsx#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L24) | - | -| | [helpers.test.tsx#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L43) | - | -| | [helpers.test.tsx#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L49) | - | -| | [helpers.test.tsx#L368](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L368) | - | | | [model.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L8) | - | | | [model.ts#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L30) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L33) | - | @@ -1709,10 +1715,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L27) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L28) | - | -| | [field_value_lists.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx#L12) | - | -| | [field_value_lists.tsx#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx#L20) | - | -| | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L13) | - | -| | [field_value_match_any.tsx#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L22) | - | | | [helpers.test.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L11) | - | | | [helpers.test.ts#L333](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L333) | - | | | [helpers.test.ts#L339](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L339) | - | @@ -1721,10 +1723,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [helpers.test.ts#L363](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L363) | - | | | [helpers.test.ts#L371](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L371) | - | | | [helpers.test.ts#L380](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L380) | - | -| | [operator.tsx#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx#L11) | - | -| | [operator.tsx#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx#L17) | - | -| | [helpers.test.tsx#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L43) | - | -| | [helpers.test.tsx#L104](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L104) | - | +| | [helpers.test.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts#L1) | - | +| | [helpers.test.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts#L2) | - | | | [index.tsx#L32](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L32) | - | | | [index.tsx#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L86) | - | | | [index.tsx#L305](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L305) | - | @@ -1767,10 +1767,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L27) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L28) | - | -| | [field_value_lists.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx#L12) | - | -| | [field_value_lists.tsx#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx#L20) | - | -| | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L13) | - | -| | [field_value_match_any.tsx#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L22) | - | | | [helpers.test.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L11) | - | | | [helpers.test.ts#L333](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L333) | - | | | [helpers.test.ts#L339](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L339) | - | @@ -1779,10 +1775,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [helpers.test.ts#L363](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L363) | - | | | [helpers.test.ts#L371](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L371) | - | | | [helpers.test.ts#L380](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L380) | - | -| | [operator.tsx#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx#L11) | - | -| | [operator.tsx#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx#L17) | - | -| | [helpers.test.tsx#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L43) | - | -| | [helpers.test.tsx#L104](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L104) | - | +| | [helpers.test.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts#L1) | - | +| | [helpers.test.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts#L2) | - | | | [index.tsx#L32](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L32) | - | | | [index.tsx#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L86) | - | | | [index.tsx#L305](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L305) | - | @@ -1791,12 +1785,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.tsx#L242](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/index.tsx#L242) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx#L12) | - | | | [index.tsx#L35](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/autocomplete_field/index.tsx#L35) | - | +| | [types.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L21) | - | +| | [types.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L66) | - | +| | [action.ts#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L19) | - | +| | [action.ts#L100](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L100) | - | | | [index.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L8) | - | | | [index.ts#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#L86) | - | -| | [types.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L20) | - | -| | [types.ts#L64](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#L64) | - | -| | [action.ts#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L17) | - | -| | [action.ts#L98](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#L98) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#L12) | - | | | [index.tsx#L48](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#L48) | - | | | [index.tsx#L122](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#L122) | - | @@ -1830,9 +1824,9 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [types.ts#L41](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/details/types.ts#L41) | - | | | [index.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L12) | - | | | [index.tsx#L34](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx#L34) | - | -| | [middleware.ts#L32](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L32) | - | -| | [middleware.ts#L38](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L38) | - | -| | [middleware.ts#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L43) | - | +| | [middleware.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L40) | - | +| | [middleware.ts#L55](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L55) | - | +| | [middleware.ts#L60](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#L60) | - | | | [types.ts#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L12) | - | | | [types.ts#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/types.ts#L28) | - | | | [index.tsx#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/description_step/index.tsx#L15) | - | @@ -1845,13 +1839,30 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.tsx#L44](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.tsx#L44) | - | | | [index.tsx#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx#L21) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/event_counts/index.tsx#L33) | - | -| | [index.tsx#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx#L9) | - | -| | [index.tsx#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/kuery_autocompletion/index.tsx#L21) | - | -| | [get_query_filter.ts#L15](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L15) | - | -| | [get_query_filter.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L31) | - | +| | [index.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts#L1) | - | +| | [index.d.ts#L66](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts#L66) | - | +| | [index.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts#L1) | - | +| | [index.d.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts#L10) | - | +| | [index.d.ts#L23](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/containers/source/index.d.ts#L23) | - | +| | [types.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts#L3) | - | +| | [types.d.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts#L25) | - | +| | [types.d.ts#L55](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/url_state/types.d.ts#L55) | - | +| | [columns.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts#L1) | - | +| | [columns.d.ts#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts#L20) | - | +| | [columns.d.ts#L21](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/columns.d.ts#L21) | - | +| | [index.d.ts#L3](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts#L3) | - | +| | [index.d.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/search_bar/index.d.ts#L9) | - | +| | [types.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts#L2) | - | +| | [types.d.ts#L40](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/hosts/pages/details/types.d.ts#L40) | - | +| | [index.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts#L2) | - | +| | [index.d.ts#L10](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/components/network_top_countries_table/index.d.ts#L10) | - | +| | [types.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts#L1) | - | +| | [types.d.ts#L25](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/network/pages/details/types.d.ts#L25) | - | | | [types.ts#L9](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts#L9) | - | | | [types.ts#L35](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts#L35) | - | | | [types.ts#L53](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/network/pages/navigation/types.ts#L53) | - | +| | [get_query_filter.ts#L16](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L16) | - | +| | [get_query_filter.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts#L31) | - | | | [helpers.tsx#L39](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx#L39) | - | | | [helpers.tsx#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx#L49) | - | | | [helpers.tsx#L52](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx#L52) | - | @@ -1870,11 +1881,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L31](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L31) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L30) | - | -| | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L13) | - | -| | [field_value_match_any.tsx#L24](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L24) | - | -| | [helpers.test.tsx#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L43) | - | -| | [helpers.test.tsx#L49](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L49) | - | -| | [helpers.test.tsx#L368](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L368) | - | | | [model.ts#L8](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L8) | - | | | [model.ts#L30](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts#L30) | - | | | [index.tsx#L33](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L33) | - | @@ -1921,10 +1927,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [use_field_value_autocomplete.ts#L27](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/hooks/use_field_value_autocomplete.ts#L27) | - | | | [field_value_match.tsx#L19](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L19) | - | | | [field_value_match.tsx#L28](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx#L28) | - | -| | [field_value_lists.tsx#L12](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx#L12) | - | -| | [field_value_lists.tsx#L20](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx#L20) | - | -| | [field_value_match_any.tsx#L13](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L13) | - | -| | [field_value_match_any.tsx#L22](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match_any.tsx#L22) | - | | | [helpers.test.ts#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L11) | - | | | [helpers.test.ts#L333](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L333) | - | | | [helpers.test.ts#L339](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L339) | - | @@ -1933,10 +1935,8 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [helpers.test.ts#L363](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L363) | - | | | [helpers.test.ts#L371](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L371) | - | | | [helpers.test.ts#L380](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts#L380) | - | -| | [operator.tsx#L11](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx#L11) | - | -| | [operator.tsx#L17](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/autocomplete/operator.tsx#L17) | - | -| | [helpers.test.tsx#L43](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L43) | - | -| | [helpers.test.tsx#L104](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx#L104) | - | +| | [helpers.test.d.ts#L1](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts#L1) | - | +| | [helpers.test.d.ts#L2](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/target/types/public/common/components/exceptions/helpers.test.d.ts#L2) | - | | | [index.tsx#L32](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L32) | - | | | [index.tsx#L86](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L86) | - | | | [index.tsx#L305](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/index.tsx#L305) | - | diff --git a/api_docs/features.json b/api_docs/features.json index 6881b65061fac9..63c3ebe3a92dec 100644 --- a/api_docs/features.json +++ b/api_docs/features.json @@ -1599,13 +1599,6 @@ "path": "x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts", "lineNumber": 311 } - }, - { - "plugin": "beatsManagement", - "link": { - "path": "x-pack/plugins/beats_management/server/plugin.ts", - "lineNumber": 61 - } } ] }, @@ -3298,13 +3291,6 @@ "path": "x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts", "lineNumber": 311 } - }, - { - "plugin": "beatsManagement", - "link": { - "path": "x-pack/plugins/beats_management/server/plugin.ts", - "lineNumber": 61 - } } ] }, diff --git a/api_docs/fleet.json b/api_docs/fleet.json index 94f2acc12adf7e..389a56cccefc5b 100644 --- a/api_docs/fleet.json +++ b/api_docs/fleet.json @@ -8837,7 +8837,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 113 + "lineNumber": 114 }, "deprecated": false, "children": [ @@ -8855,7 +8855,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 117 + "lineNumber": 118 }, "deprecated": false }, @@ -8870,7 +8870,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 121 + "lineNumber": 122 }, "deprecated": false }, @@ -8885,7 +8885,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 125 + "lineNumber": 126 }, "deprecated": false }, @@ -8900,7 +8900,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 129 + "lineNumber": 130 }, "deprecated": false }, @@ -8918,7 +8918,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 133 + "lineNumber": 134 }, "deprecated": false }, @@ -8933,7 +8933,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 139 + "lineNumber": 140 }, "deprecated": false } @@ -9085,7 +9085,7 @@ "label": "agent", "description": [], "signature": [ - "{ monitoring: { use_output?: string | undefined; enabled: boolean; metrics: boolean; logs: boolean; }; } | undefined" + "{ monitoring: { namespace?: string | undefined; use_output?: string | undefined; enabled: boolean; metrics: boolean; logs: boolean; }; } | undefined" ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", @@ -9326,7 +9326,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 102 + "lineNumber": 103 }, "deprecated": false, "children": [ @@ -9342,7 +9342,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 103 + "lineNumber": 104 }, "deprecated": false }, @@ -9355,7 +9355,7 @@ "description": [], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 104 + "lineNumber": 105 }, "deprecated": false }, @@ -9371,7 +9371,7 @@ ], "source": { "path": "x-pack/plugins/fleet/common/types/models/agent_policy.ts", - "lineNumber": 105 + "lineNumber": 106 }, "deprecated": false } diff --git a/api_docs/home.json b/api_docs/home.json index bc99a31a09c302..2aa23c1b756dfa 100644 --- a/api_docs/home.json +++ b/api_docs/home.json @@ -953,13 +953,6 @@ }, "deprecated": true, "references": [ - { - "plugin": "cloud", - "link": { - "path": "x-pack/plugins/cloud/public/plugin.ts", - "lineNumber": 66 - } - }, { "plugin": "ml", "link": { @@ -973,6 +966,13 @@ "path": "x-pack/plugins/apm/public/plugin.ts", "lineNumber": 82 } + }, + { + "plugin": "cloud", + "link": { + "path": "x-pack/plugins/cloud/public/plugin.ts", + "lineNumber": 66 + } } ] } diff --git a/api_docs/kibana_utils.json b/api_docs/kibana_utils.json index ade7a843a68413..173348ea2f2638 100644 --- a/api_docs/kibana_utils.json +++ b/api_docs/kibana_utils.json @@ -3799,7 +3799,7 @@ "description": [], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 717 + "lineNumber": 704 }, "deprecated": false }, @@ -3822,7 +3822,7 @@ ], "source": { "path": "src/core/public/application/types.ts", - "lineNumber": 717 + "lineNumber": 704 }, "deprecated": false } diff --git a/api_docs/licensing.json b/api_docs/licensing.json index f3ec50895547f6..873f40bd301a1c 100644 --- a/api_docs/licensing.json +++ b/api_docs/licensing.json @@ -554,192 +554,192 @@ "deprecated": true, "references": [ { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", - "lineNumber": 22 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 19 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", - "lineNumber": 23 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 36 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", - "lineNumber": 24 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 53 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", - "lineNumber": 159 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 70 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", - "lineNumber": 161 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 87 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", - "lineNumber": 42 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 104 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", - "lineNumber": 43 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 122 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", - "lineNumber": 44 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 139 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", - "lineNumber": 54 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 156 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", - "lineNumber": 55 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 173 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", - "lineNumber": 47 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 190 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", - "lineNumber": 48 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 207 } }, { - "plugin": "apm", + "plugin": "security", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 19 + "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", + "lineNumber": 169 } }, { - "plugin": "apm", + "plugin": "security", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 36 + "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", + "lineNumber": 194 } }, { - "plugin": "apm", + "plugin": "security", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 53 + "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", + "lineNumber": 219 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 70 + "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", + "lineNumber": 22 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 87 + "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", + "lineNumber": 23 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 104 + "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", + "lineNumber": 24 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 122 + "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", + "lineNumber": 47 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 139 + "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", + "lineNumber": 48 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 156 + "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", + "lineNumber": 42 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 173 + "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", + "lineNumber": 43 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 190 + "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", + "lineNumber": 44 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 207 + "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", + "lineNumber": 54 } }, { - "plugin": "security", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", - "lineNumber": 169 + "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", + "lineNumber": 55 } }, { - "plugin": "security", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", - "lineNumber": 194 + "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", + "lineNumber": 159 } }, { - "plugin": "security", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", - "lineNumber": 219 + "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", + "lineNumber": 161 } } ] @@ -942,73 +942,66 @@ "deprecated": true, "references": [ { - "plugin": "security", - "link": { - "path": "x-pack/plugins/security/public/plugin.tsx", - "lineNumber": 82 - } - }, - { - "plugin": "licenseManagement", + "plugin": "reporting", "link": { - "path": "x-pack/plugins/license_management/public/plugin.ts", - "lineNumber": 61 + "path": "x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx", + "lineNumber": 40 } }, { - "plugin": "ml", + "plugin": "reporting", "link": { - "path": "x-pack/plugins/ml/public/application/license/check_license.tsx", - "lineNumber": 26 + "path": "x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx", + "lineNumber": 33 } }, { - "plugin": "ml", + "plugin": "reporting", "link": { - "path": "x-pack/plugins/ml/public/plugin.ts", - "lineNumber": 136 + "path": "x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx", + "lineNumber": 81 } }, { - "plugin": "fleet", + "plugin": "reporting", "link": { - "path": "x-pack/plugins/fleet/public/plugin.ts", - "lineNumber": 97 + "path": "x-pack/plugins/reporting/public/plugin.ts", + "lineNumber": 116 } }, { "plugin": "reporting", "link": { - "path": "x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx", - "lineNumber": 40 + "path": "x-pack/plugins/reporting/public/components/report_listing.tsx", + "lineNumber": 61 } }, { - "plugin": "reporting", + "plugin": "licenseManagement", "link": { - "path": "x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx", - "lineNumber": 33 + "path": "x-pack/plugins/license_management/public/plugin.ts", + "lineNumber": 61 } }, { - "plugin": "reporting", + "plugin": "security", "link": { - "path": "x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx", - "lineNumber": 81 + "path": "x-pack/plugins/security/public/plugin.tsx", + "lineNumber": 82 } }, { - "plugin": "reporting", + "plugin": "ml", "link": { - "path": "x-pack/plugins/reporting/public/plugin.ts", - "lineNumber": 116 + "path": "x-pack/plugins/ml/public/application/license/check_license.tsx", + "lineNumber": 26 } }, { - "plugin": "reporting", + "plugin": "ml", "link": { - "path": "x-pack/plugins/reporting/public/components/report_listing.tsx", - "lineNumber": 61 + "path": "x-pack/plugins/ml/public/plugin.ts", + "lineNumber": 136 } }, { @@ -1019,17 +1012,17 @@ } }, { - "plugin": "beatsManagement", + "plugin": "crossClusterReplication", "link": { - "path": "x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts", - "lineNumber": 65 + "path": "x-pack/plugins/cross_cluster_replication/public/plugin.ts", + "lineNumber": 75 } }, { - "plugin": "crossClusterReplication", + "plugin": "fleet", "link": { - "path": "x-pack/plugins/cross_cluster_replication/public/plugin.ts", - "lineNumber": 75 + "path": "x-pack/plugins/fleet/public/plugin.ts", + "lineNumber": 97 } }, { @@ -2650,192 +2643,192 @@ "deprecated": true, "references": [ { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", - "lineNumber": 22 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 19 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", - "lineNumber": 23 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 36 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", - "lineNumber": 24 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 53 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", - "lineNumber": 159 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 70 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", - "lineNumber": 161 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 87 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", - "lineNumber": 42 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 104 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", - "lineNumber": 43 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 122 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", - "lineNumber": 44 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 139 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", - "lineNumber": 54 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 156 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", - "lineNumber": 55 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 173 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", - "lineNumber": 47 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 190 } }, { - "plugin": "securitySolution", + "plugin": "apm", "link": { - "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", - "lineNumber": 48 + "path": "x-pack/plugins/apm/common/license_check.test.ts", + "lineNumber": 207 } }, { - "plugin": "apm", + "plugin": "security", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 19 + "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", + "lineNumber": 169 } }, { - "plugin": "apm", + "plugin": "security", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 36 + "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", + "lineNumber": 194 } }, { - "plugin": "apm", + "plugin": "security", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 53 + "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", + "lineNumber": 219 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 70 + "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", + "lineNumber": 22 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 87 + "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", + "lineNumber": 23 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 104 + "path": "x-pack/plugins/security_solution/common/license/policy_config.test.ts", + "lineNumber": 24 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 122 + "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", + "lineNumber": 47 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 139 + "path": "x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts", + "lineNumber": 48 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 156 + "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", + "lineNumber": 42 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 173 + "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", + "lineNumber": 43 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 190 + "path": "x-pack/plugins/security_solution/server/endpoint/lib/policy/license_watch.test.ts", + "lineNumber": 44 } }, { - "plugin": "apm", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/apm/common/license_check.test.ts", - "lineNumber": 207 + "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", + "lineNumber": 54 } }, { - "plugin": "security", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", - "lineNumber": 169 + "path": "x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts", + "lineNumber": 55 } }, { - "plugin": "security", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", - "lineNumber": 194 + "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", + "lineNumber": 159 } }, { - "plugin": "security", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/security/common/licensing/license_service.test.ts", - "lineNumber": 219 + "path": "x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts", + "lineNumber": 161 } } ] @@ -3060,7 +3053,7 @@ "plugin": "spaces", "link": { "path": "x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts", - "lineNumber": 442 + "lineNumber": 431 } }, { @@ -3126,13 +3119,6 @@ "lineNumber": 139 } }, - { - "plugin": "fleet", - "link": { - "path": "x-pack/plugins/fleet/server/plugin.ts", - "lineNumber": 213 - } - }, { "plugin": "indexManagement", "link": { @@ -3147,6 +3133,13 @@ "lineNumber": 73 } }, + { + "plugin": "fleet", + "link": { + "path": "x-pack/plugins/fleet/server/plugin.ts", + "lineNumber": 213 + } + }, { "plugin": "graph", "link": { diff --git a/api_docs/lists.json b/api_docs/lists.json index 8932650bdff76a..90f5cd726287b9 100644 --- a/api_docs/lists.json +++ b/api_docs/lists.json @@ -273,1055 +273,8 @@ "initialIsOpen": false } ], - "functions": [ - { - "parentPluginId": "lists", - "id": "def-public.addEndpointExceptionListWithValidation", - "type": "Function", - "tags": [], - "label": "addEndpointExceptionListWithValidation", - "description": [], - "signature": [ - "({ http, signal, }: ", - "AddEndpointExceptionListProps", - ") => 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\" | \"endpoint_events\"; updated_at: string; updated_by: string; version: number; } | {}>" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/api.ts", - "lineNumber": 532 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.addEndpointExceptionListWithValidation.$1", - "type": "Object", - "tags": [], - "label": "{\n http,\n signal,\n}", - "description": [], - "signature": [ - "AddEndpointExceptionListProps" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/api.ts", - "lineNumber": 532 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.addExceptionListWithValidation", - "type": "Function", - "tags": [], - "label": "addExceptionListWithValidation", - "description": [], - "signature": [ - "({ http, list, signal, }: ", - "AddExceptionListProps", - ") => 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\" | \"endpoint_events\"; updated_at: string; updated_by: string; version: number; }>" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/api.ts", - "lineNumber": 66 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.addExceptionListWithValidation.$1", - "type": "Object", - "tags": [], - "label": "{\n http,\n list,\n signal,\n}", - "description": [], - "signature": [ - "AddExceptionListProps" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/api.ts", - "lineNumber": 66 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.exportListWithValidation", - "type": "Function", - "tags": [], - "label": "exportListWithValidation", - "description": [], - "signature": [ - "({ http, listId, signal, }: ", - "ExportListParams", - ") => Promise" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/api.ts", - "lineNumber": 174 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.exportListWithValidation.$1", - "type": "Object", - "tags": [], - "label": "{\n http,\n listId,\n signal,\n}", - "description": [], - "signature": [ - "ExportListParams" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/api.ts", - "lineNumber": 174 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.fetchExceptionListByIdWithValidation", - "type": "Function", - "tags": [], - "label": "fetchExceptionListByIdWithValidation", - "description": [], - "signature": [ - "({ http, id, namespaceType, signal, }: ", - "ApiCallByIdProps", - ") => 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\" | \"endpoint_events\"; updated_at: string; updated_by: string; version: number; }>" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/api.ts", - "lineNumber": 299 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.fetchExceptionListByIdWithValidation.$1", - "type": "Object", - "tags": [], - "label": "{\n http,\n id,\n namespaceType,\n signal,\n}", - "description": [], - "signature": [ - "ApiCallByIdProps" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/api.ts", - "lineNumber": 299 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.transformNewItemOutput", - "type": "Function", - "tags": [], - "label": "transformNewItemOutput", - "description": [], - "signature": [ - "(exceptionItem: { description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }) => { description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/transforms.ts", - "lineNumber": 40 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.transformNewItemOutput.$1", - "type": "CompoundType", - "tags": [], - "label": "exceptionItem", - "description": [], - "signature": [ - "{ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/transforms.ts", - "lineNumber": 41 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.transformOutput", - "type": "Function", - "tags": [], - "label": "transformOutput", - "description": [ - "\nTransforms the output of exception items to compensate for technical debt or UI concerns such as\nReactJS preferences for having ids within arrays if the data is not modeled that way.\n\nIf you add a new transform of the output called \"myNewTransform\" do it\nin the form of:\nflow(removeIdFromExceptionItemsEntries, myNewTransform)(exceptionItem)\n" - ], - "signature": [ - "(exceptionItem: { _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; } | ({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; name: string; type: \"simple\"; } & { _version?: string | undefined; comments?: ({ comment: string; } & { id?: string | undefined; })[] | undefined; id?: string | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; })) => { _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; } | ({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; name: string; type: \"simple\"; } & { _version?: string | undefined; comments?: ({ comment: string; } & { id?: string | undefined; })[] | undefined; id?: string | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; })" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/transforms.ts", - "lineNumber": 35 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.transformOutput.$1", - "type": "CompoundType", - "tags": [], - "label": "exceptionItem", - "description": [], - "signature": [ - "{ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; } | ({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; name: string; type: \"simple\"; } & { _version?: string | undefined; comments?: ({ comment: string; } & { id?: string | undefined; })[] | undefined; id?: string | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; })" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/transforms.ts", - "lineNumber": 36 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [ - "The exceptionItem transformed from the output" - ], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useApi", - "type": "Function", - "tags": [], - "label": "useApi", - "description": [], - "signature": [ - "(http: ", - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreHttpPluginApi", - "section": "def-public.HttpSetup", - "text": "HttpSetup" - }, - ") => ", - "ExceptionsApi" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/hooks/use_api.ts", - "lineNumber": 41 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.useApi.$1", - "type": "Object", - "tags": [], - "label": "http", - "description": [], - "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreHttpPluginApi", - "section": "def-public.HttpSetup", - "text": "HttpSetup" - } - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/hooks/use_api.ts", - "lineNumber": 41 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useAsync", - "type": "Function", - "tags": [], - "label": "useAsync", - "description": [ - "\n" - ], - "signature": [ - "(fn: (...args: Args) => Promise) => ", - "Async", - "" - ], - "source": { - "path": "x-pack/plugins/lists/public/common/hooks/use_async.ts", - "lineNumber": 25 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.useAsync.$1", - "type": "Function", - "tags": [], - "label": "fn", - "description": [], - "signature": [ - "(...args: Args) => Promise" - ], - "source": { - "path": "x-pack/plugins/lists/public/common/hooks/use_async.ts", - "lineNumber": 26 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [ - "An {@link AsyncTask} containing the underlying task's state along with a start callback" - ], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useCreateListIndex", - "type": "Function", - "tags": [], - "label": "useCreateListIndex", - "description": [], - "signature": [ - "() => ", - "Async", - "<[args: ", - "OptionalSignalArgs", - "<", - "ApiParams", - ">], { acknowledged: boolean; }>" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/hooks/use_create_list_index.ts", - "lineNumber": 15 - }, - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useCursor", - "type": "Function", - "tags": [], - "label": "useCursor", - "description": [], - "signature": [ - "({ pageIndex, pageSize }: ", - "UseCursorProps", - ") => [string | undefined, SetCursor]" - ], - "source": { - "path": "x-pack/plugins/lists/public/common/hooks/use_cursor.ts", - "lineNumber": 20 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.useCursor.$1", - "type": "Object", - "tags": [], - "label": "{ pageIndex, pageSize }", - "description": [], - "signature": [ - "UseCursorProps" - ], - "source": { - "path": "x-pack/plugins/lists/public/common/hooks/use_cursor.ts", - "lineNumber": 20 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useDeleteList", - "type": "Function", - "tags": [], - "label": "useDeleteList", - "description": [], - "signature": [ - "() => ", - "Async", - "<[args: ", - "OptionalSignalArgs", - "<", - "DeleteListParams", - ">], { _version: string | undefined; created_at: string; created_by: string; description: string; deserializer: string | undefined; id: string; immutable: boolean; meta: object | undefined; name: string; serializer: string | undefined; tie_breaker_id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; updated_at: string; updated_by: string; version: number; }>" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/hooks/use_delete_list.ts", - "lineNumber": 15 - }, - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useExceptionListItems", - "type": "Function", - "tags": [], - "label": "useExceptionListItems", - "description": [ - "\nHook for using to get an ExceptionList and it's ExceptionListItems\n" - ], - "signature": [ - "({ http, lists, pagination, filterOptions, showDetectionsListsOnly, showEndpointListsOnly, matchFilters, onError, onSuccess, }: ", - "UseExceptionListProps", - ") => ", - "ReturnExceptionListAndItems" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.ts", - "lineNumber": 39 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.useExceptionListItems.$1", - "type": "Object", - "tags": [], - "label": "{\n http,\n lists,\n pagination = {\n page: 1,\n perPage: 20,\n total: 0,\n },\n filterOptions,\n showDetectionsListsOnly,\n showEndpointListsOnly,\n matchFilters,\n onError,\n onSuccess,\n}", - "description": [], - "signature": [ - "UseExceptionListProps" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/hooks/use_exception_list_items.ts", - "lineNumber": 39 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useExceptionLists", - "type": "Function", - "tags": [], - "label": "useExceptionLists", - "description": [ - "\nHook for fetching ExceptionLists\n" - ], - "signature": [ - "({ errorMessage, http, pagination, filterOptions, namespaceTypes, notifications, showTrustedApps, }: ", - "UseExceptionListsProps", - ") => ", - "ReturnExceptionLists" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.ts", - "lineNumber": 30 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.useExceptionLists.$1", - "type": "Object", - "tags": [], - "label": "{\n errorMessage,\n http,\n pagination = {\n page: 1,\n perPage: 20,\n total: 0,\n },\n filterOptions = {},\n namespaceTypes,\n notifications,\n showTrustedApps = false,\n}", - "description": [], - "signature": [ - "UseExceptionListsProps" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/hooks/use_exception_lists.ts", - "lineNumber": 30 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useExportList", - "type": "Function", - "tags": [], - "label": "useExportList", - "description": [], - "signature": [ - "() => ", - "Async", - "<[args: ", - "OptionalSignalArgs", - "<", - "ExportListParams", - ">], Blob>" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/hooks/use_export_list.ts", - "lineNumber": 15 - }, - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useFindLists", - "type": "Function", - "tags": [], - "label": "useFindLists", - "description": [], - "signature": [ - "() => ", - "Async", - "<[args: ", - "OptionalSignalArgs", - "<", - "FindListsParams", - ">], { cursor: string; data: { _version: string | undefined; created_at: string; created_by: string; description: string; deserializer: string | undefined; id: string; immutable: boolean; meta: object | undefined; name: string; serializer: string | undefined; tie_breaker_id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; updated_at: string; updated_by: string; version: number; }[]; page: number; per_page: number; total: number; }>" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/hooks/use_find_lists.ts", - "lineNumber": 15 - }, - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useImportList", - "type": "Function", - "tags": [], - "label": "useImportList", - "description": [], - "signature": [ - "() => ", - "Async", - "<[args: ", - "OptionalSignalArgs", - "<", - "ImportListParams", - ">], { _version: string | undefined; created_at: string; created_by: string; description: string; deserializer: string | undefined; id: string; immutable: boolean; meta: object | undefined; name: string; serializer: string | undefined; tie_breaker_id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; updated_at: string; updated_by: string; version: number; }>" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/hooks/use_import_list.ts", - "lineNumber": 15 - }, - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useReadListIndex", - "type": "Function", - "tags": [], - "label": "useReadListIndex", - "description": [], - "signature": [ - "() => ", - "Async", - "<[args: ", - "OptionalSignalArgs", - "<", - "ApiParams", - ">], { list_index: boolean; list_item_index: boolean; }>" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/hooks/use_read_list_index.ts", - "lineNumber": 15 - }, - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.useReadListPrivileges", - "type": "Function", - "tags": [], - "label": "useReadListPrivileges", - "description": [], - "signature": [ - "() => ", - "Async", - "<[args: ", - "OptionalSignalArgs", - "<", - "ApiParams", - ">], unknown>" - ], - "source": { - "path": "x-pack/plugins/lists/public/lists/hooks/use_read_list_privileges.ts", - "lineNumber": 15 - }, - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.withOptionalSignal", - "type": "Function", - "tags": [], - "label": "withOptionalSignal", - "description": [ - "\n" - ], - "signature": [ - "(fn: (args: Args) => Result) => (args: ", - "OptionalSignalArgs", - ") => Result" - ], - "source": { - "path": "x-pack/plugins/lists/public/common/with_optional_signal.ts", - "lineNumber": 20 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.withOptionalSignal.$1", - "type": "Function", - "tags": [], - "label": "fn", - "description": [], - "signature": [ - "(args: Args) => Result" - ], - "source": { - "path": "x-pack/plugins/lists/public/common/with_optional_signal.ts", - "lineNumber": 20 - }, - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [ - "An async function where the AbortSignal argument is optional" - ], - "initialIsOpen": false - } - ], - "interfaces": [ - { - "parentPluginId": "lists", - "id": "def-public.ExceptionList", - "type": "Interface", - "tags": [], - "label": "ExceptionList", - "description": [], - "signature": [ - { - "pluginId": "lists", - "scope": "public", - "docId": "kibListsPluginApi", - "section": "def-public.ExceptionList", - "text": "ExceptionList" - }, - " extends { _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\" | \"endpoint_events\"; updated_at: string; updated_by: string; version: number; }" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 42 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.ExceptionList.totalItems", - "type": "number", - "tags": [], - "label": "totalItems", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 43 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListFilter", - "type": "Interface", - "tags": [], - "label": "ExceptionListFilter", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 126 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListFilter.name", - "type": "CompoundType", - "tags": [], - "label": "name", - "description": [], - "signature": [ - "string | null | undefined" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 127 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListFilter.list_id", - "type": "CompoundType", - "tags": [], - "label": "list_id", - "description": [], - "signature": [ - "string | null | undefined" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 128 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListFilter.created_by", - "type": "CompoundType", - "tags": [], - "label": "created_by", - "description": [], - "signature": [ - "string | null | undefined" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 129 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListFilter.type", - "type": "CompoundType", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "string | null | undefined" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 130 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListFilter.tags", - "type": "CompoundType", - "tags": [], - "label": "tags", - "description": [], - "signature": [ - "string | null | undefined" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 131 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListIdentifiers", - "type": "Interface", - "tags": [], - "label": "ExceptionListIdentifiers", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 63 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListIdentifiers.id", - "type": "string", - "tags": [], - "label": "id", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 64 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListIdentifiers.listId", - "type": "string", - "tags": [], - "label": "listId", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 65 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListIdentifiers.namespaceType", - "type": "CompoundType", - "tags": [], - "label": "namespaceType", - "description": [], - "signature": [ - "\"single\" | \"agnostic\"" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 66 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.ExceptionListIdentifiers.type", - "type": "CompoundType", - "tags": [], - "label": "type", - "description": [], - "signature": [ - "\"endpoint\" | \"detection\" | \"endpoint_events\"" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 67 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.Pagination", - "type": "Interface", - "tags": [], - "label": "Pagination", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 29 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.Pagination.page", - "type": "number", - "tags": [], - "label": "page", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 30 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.Pagination.perPage", - "type": "number", - "tags": [], - "label": "perPage", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 31 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.Pagination.total", - "type": "number", - "tags": [], - "label": "total", - "description": [], - "signature": [ - "number | undefined" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 32 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.UseExceptionListItemsSuccess", - "type": "Interface", - "tags": [], - "label": "UseExceptionListItemsSuccess", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 46 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.UseExceptionListItemsSuccess.exceptions", - "type": "Array", - "tags": [], - "label": "exceptions", - "description": [], - "signature": [ - "{ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; }[]" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 47 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.UseExceptionListItemsSuccess.pagination", - "type": "Object", - "tags": [], - "label": "pagination", - "description": [], - "signature": [ - { - "pluginId": "lists", - "scope": "public", - "docId": "kibListsPluginApi", - "section": "def-public.Pagination", - "text": "Pagination" - } - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 48 - }, - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-public.UseExceptionListsSuccess", - "type": "Interface", - "tags": [], - "label": "UseExceptionListsSuccess", - "description": [], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 121 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-public.UseExceptionListsSuccess.exceptions", - "type": "Array", - "tags": [], - "label": "exceptions", - "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\" | \"endpoint_events\"; updated_at: string; updated_by: string; version: number; }[]" - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 122 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-public.UseExceptionListsSuccess.pagination", - "type": "Object", - "tags": [], - "label": "pagination", - "description": [], - "signature": [ - { - "pluginId": "lists", - "scope": "public", - "docId": "kibListsPluginApi", - "section": "def-public.Pagination", - "text": "Pagination" - } - ], - "source": { - "path": "x-pack/plugins/lists/public/exceptions/types.ts", - "lineNumber": 123 - }, - "deprecated": false - } - ], - "initialIsOpen": false - } - ], + "functions": [], + "interfaces": [], "enums": [], "misc": [], "objects": [ @@ -1338,7 +291,7 @@ ], "source": { "path": "x-pack/plugins/lists/public/shared_exports.ts", - "lineNumber": 36 + "lineNumber": 10 }, "deprecated": false, "initialIsOpen": false @@ -1388,7 +341,7 @@ "description": [], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 57 + "lineNumber": 56 }, "deprecated": false, "children": [ @@ -1404,7 +357,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 62 + "lineNumber": 61 }, "deprecated": false, "children": [ @@ -1420,7 +373,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 62 + "lineNumber": 61 }, "deprecated": false, "isRequired": true @@ -1442,7 +395,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 67 + "lineNumber": 66 }, "deprecated": false, "children": [ @@ -1458,7 +411,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 67 + "lineNumber": 66 }, "deprecated": false, "isRequired": true @@ -1480,7 +433,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 76 + "lineNumber": 75 }, "deprecated": false, "children": [ @@ -1496,7 +449,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 76 + "lineNumber": 75 }, "deprecated": false, "isRequired": true @@ -1518,7 +471,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 91 + "lineNumber": 90 }, "deprecated": false, "children": [], @@ -1540,7 +493,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 103 + "lineNumber": 102 }, "deprecated": false, "children": [], @@ -1560,7 +513,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 115 + "lineNumber": 114 }, "deprecated": false, "children": [], @@ -1582,7 +535,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 129 + "lineNumber": 128 }, "deprecated": false, "children": [ @@ -1598,7 +551,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 129 + "lineNumber": 128 }, "deprecated": false, "isRequired": true @@ -1622,7 +575,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 165 + "lineNumber": 164 }, "deprecated": false, "children": [ @@ -1638,7 +591,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 165 + "lineNumber": 164 }, "deprecated": false, "isRequired": true @@ -1662,7 +615,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 201 + "lineNumber": 200 }, "deprecated": false, "children": [ @@ -1678,7 +631,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 201 + "lineNumber": 200 }, "deprecated": false, "isRequired": true @@ -1700,7 +653,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 209 + "lineNumber": 208 }, "deprecated": false, "children": [ @@ -1716,7 +669,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 209 + "lineNumber": 208 }, "deprecated": false, "isRequired": true @@ -1738,7 +691,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 236 + "lineNumber": 235 }, "deprecated": false, "children": [ @@ -1754,7 +707,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 236 + "lineNumber": 235 }, "deprecated": false, "isRequired": true @@ -1776,7 +729,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 267 + "lineNumber": 266 }, "deprecated": false, "children": [ @@ -1792,7 +745,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 267 + "lineNumber": 266 }, "deprecated": false, "isRequired": true @@ -1820,7 +773,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 281 + "lineNumber": 280 }, "deprecated": false, "children": [ @@ -1842,7 +795,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 281 + "lineNumber": 280 }, "deprecated": false, "isRequired": true @@ -1870,7 +823,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 312 + "lineNumber": 311 }, "deprecated": false, "children": [ @@ -1892,7 +845,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 312 + "lineNumber": 311 }, "deprecated": false, "isRequired": true @@ -1914,7 +867,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 345 + "lineNumber": 344 }, "deprecated": false, "children": [ @@ -1930,7 +883,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 345 + "lineNumber": 344 }, "deprecated": false, "isRequired": true @@ -1952,7 +905,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 359 + "lineNumber": 358 }, "deprecated": false, "children": [ @@ -1968,7 +921,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 359 + "lineNumber": 358 }, "deprecated": false, "isRequired": true @@ -1992,7 +945,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 374 + "lineNumber": 373 }, "deprecated": false, "children": [ @@ -2008,7 +961,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 374 + "lineNumber": 373 }, "deprecated": false, "isRequired": true @@ -2030,7 +983,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 387 + "lineNumber": 386 }, "deprecated": false, "children": [ @@ -2046,7 +999,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 387 + "lineNumber": 386 }, "deprecated": false, "isRequired": true @@ -2068,7 +1021,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 409 + "lineNumber": 408 }, "deprecated": false, "children": [ @@ -2084,7 +1037,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 409 + "lineNumber": 408 }, "deprecated": false, "isRequired": true @@ -2106,7 +1059,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 431 + "lineNumber": 430 }, "deprecated": false, "children": [ @@ -2122,7 +1075,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 431 + "lineNumber": 430 }, "deprecated": false, "isRequired": true @@ -2144,7 +1097,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 449 + "lineNumber": 448 }, "deprecated": false, "children": [ @@ -2160,7 +1113,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 449 + "lineNumber": 448 }, "deprecated": false, "isRequired": true @@ -2184,7 +1137,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 477 + "lineNumber": 476 }, "deprecated": false, "children": [ @@ -2200,7 +1153,7 @@ ], "source": { "path": "x-pack/plugins/lists/server/services/exception_lists/exception_list_client.ts", - "lineNumber": 477 + "lineNumber": 476 }, "deprecated": false, "isRequired": true @@ -4234,228 +3187,10 @@ }, "common": { "classes": [], - "functions": [ - { - "parentPluginId": "lists", - "id": "def-common.buildExceptionFilter", - "type": "Function", - "tags": [], - "label": "buildExceptionFilter", - "description": [], - "signature": [ - "({ lists, excludeExceptions, chunkSize, }: { lists: ({ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; } | ({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }))[]; excludeExceptions: boolean; chunkSize: number; }) => ", - { - "pluginId": "data", - "scope": "common", - "docId": "kibDataPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - " | undefined" - ], - "source": { - "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", - "lineNumber": 69 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-common.buildExceptionFilter.$1.listsexcludeExceptionschunkSize", - "type": "Object", - "tags": [], - "label": "{\n lists,\n excludeExceptions,\n chunkSize,\n}", - "description": [], - "source": { - "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", - "lineNumber": 73 - }, - "deprecated": false, - "children": [ - { - "parentPluginId": "lists", - "id": "def-common.buildExceptionFilter.$1.listsexcludeExceptionschunkSize.lists", - "type": "Array", - "tags": [], - "label": "lists", - "description": [], - "signature": [ - "({ _version: string | undefined; comments: ({ comment: string; created_at: string; created_by: string; id: string; } & { updated_at?: string | undefined; updated_by?: string | undefined; })[]; created_at: string; created_by: string; description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; })[]; id: string; item_id: string; list_id: string; meta: object | undefined; name: string; namespace_type: \"single\" | \"agnostic\"; os_types: (\"windows\" | \"linux\" | \"macos\")[]; tags: string[]; tie_breaker_id: string; type: \"simple\"; updated_at: string; updated_by: string; } | ({ description: string; entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; list: { id: string; type: \"boolean\" | \"date\" | \"text\" | \"keyword\" | \"long\" | \"double\" | \"ip\" | \"date_nanos\" | \"geo_point\" | \"geo_shape\" | \"binary\" | \"short\" | \"date_range\" | \"ip_range\" | \"shape\" | \"integer\" | \"byte\" | \"float\" | \"half_float\" | \"integer_range\" | \"float_range\" | \"long_range\" | \"double_range\"; }; operator: \"excluded\" | \"included\"; type: \"list\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; } | { field: string; operator: \"excluded\" | \"included\"; type: \"wildcard\"; value: string; } | { entries: ({ field: string; operator: \"excluded\" | \"included\"; type: \"exists\"; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match\"; value: string; } | { field: string; operator: \"excluded\" | \"included\"; type: \"match_any\"; value: string[]; })[]; field: string; type: \"nested\"; })[]; list_id: string; name: string; type: \"simple\"; } & { comments?: { comment: string; }[] | undefined; item_id?: string | undefined; meta?: object | undefined; namespace_type?: \"single\" | \"agnostic\" | undefined; os_types?: (\"windows\" | \"linux\" | \"macos\")[] | undefined; tags?: string[] | undefined; }))[]" - ], - "source": { - "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", - "lineNumber": 74 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-common.buildExceptionFilter.$1.listsexcludeExceptionschunkSize.excludeExceptions", - "type": "boolean", - "tags": [], - "label": "excludeExceptions", - "description": [], - "source": { - "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", - "lineNumber": 75 - }, - "deprecated": false - }, - { - "parentPluginId": "lists", - "id": "def-common.buildExceptionFilter.$1.listsexcludeExceptionschunkSize.chunkSize", - "type": "number", - "tags": [], - "label": "chunkSize", - "description": [], - "source": { - "path": "x-pack/plugins/lists/common/exceptions/build_exceptions_filter.ts", - "lineNumber": 76 - }, - "deprecated": false - } - ] - } - ], - "returnComment": [], - "initialIsOpen": false - } - ], + "functions": [], "interfaces": [], "enums": [], - "misc": [ - { - "parentPluginId": "lists", - "id": "def-common.ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION", - "type": "string", - "tags": [], - "label": "ENDPOINT_EVENT_FILTERS_LIST_DESCRIPTION", - "description": [ - "Description of event filters agnostic list" - ], - "signature": [ - "\"Endpoint Security Event Filters List\"" - ], - "source": { - "path": "x-pack/plugins/lists/common/constants.ts", - "lineNumber": 71 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-common.ENDPOINT_EVENT_FILTERS_LIST_ID", - "type": "string", - "tags": [], - "label": "ENDPOINT_EVENT_FILTERS_LIST_ID", - "description": [ - "ID of event filters agnostic list" - ], - "signature": [ - "\"endpoint_event_filters\"" - ], - "source": { - "path": "x-pack/plugins/lists/common/constants.ts", - "lineNumber": 65 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-common.ENDPOINT_EVENT_FILTERS_LIST_NAME", - "type": "string", - "tags": [], - "label": "ENDPOINT_EVENT_FILTERS_LIST_NAME", - "description": [ - "Name of event filters agnostic list" - ], - "signature": [ - "\"Endpoint Security Event Filters List\"" - ], - "source": { - "path": "x-pack/plugins/lists/common/constants.ts", - "lineNumber": 68 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-common.ENDPOINT_LIST_ID", - "type": "string", - "tags": [], - "label": "ENDPOINT_LIST_ID", - "description": [ - "\nThis ID is used for _both_ the Saved Object ID and for the list_id\nfor the single global space agnostic endpoint list" - ], - "signature": [ - "\"endpoint_list\"" - ], - "source": { - "path": "x-pack/plugins/lists/common/constants.ts", - "lineNumber": 45 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-common.ENDPOINT_TRUSTED_APPS_LIST_ID", - "type": "string", - "tags": [], - "label": "ENDPOINT_TRUSTED_APPS_LIST_ID", - "description": [ - "ID of trusted apps agnostic list" - ], - "signature": [ - "\"endpoint_trusted_apps\"" - ], - "source": { - "path": "x-pack/plugins/lists/common/constants.ts", - "lineNumber": 56 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-common.EXCEPTION_LIST_ITEM_URL", - "type": "string", - "tags": [], - "label": "EXCEPTION_LIST_ITEM_URL", - "description": [], - "signature": [ - "\"/api/exception_lists/items\"" - ], - "source": { - "path": "x-pack/plugins/lists/common/constants.ts", - "lineNumber": 20 - }, - "deprecated": false, - "initialIsOpen": false - }, - { - "parentPluginId": "lists", - "id": "def-common.EXCEPTION_LIST_URL", - "type": "string", - "tags": [], - "label": "EXCEPTION_LIST_URL", - "description": [ - "\nException list routes" - ], - "signature": [ - "\"/api/exception_lists\"" - ], - "source": { - "path": "x-pack/plugins/lists/common/constants.ts", - "lineNumber": 19 - }, - "deprecated": false, - "initialIsOpen": false - } - ], + "misc": [], "objects": [] } } \ No newline at end of file diff --git a/api_docs/lists.mdx b/api_docs/lists.mdx index 1fe986ef4a06b3..8ef77a85fb548e 100644 --- a/api_docs/lists.mdx +++ b/api_docs/lists.mdx @@ -22,15 +22,9 @@ import listsObj from './lists.json'; ### Objects -### Functions - - ### Classes -### Interfaces - - ## Server ### Classes @@ -39,11 +33,3 @@ import listsObj from './lists.json'; ### Interfaces -## Common - -### Functions - - -### Consts, variables and types - - diff --git a/api_docs/management.json b/api_docs/management.json index 2a3bb7e3cb1eb3..41801e2f2bafc7 100644 --- a/api_docs/management.json +++ b/api_docs/management.json @@ -22,7 +22,7 @@ ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 19 + "lineNumber": 18 }, "deprecated": false, "children": [ @@ -50,7 +50,7 @@ ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 20 + "lineNumber": 19 }, "deprecated": false, "returnComment": [], @@ -88,29 +88,23 @@ "description": [], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 21 + "lineNumber": 20 }, "deprecated": false }, { "parentPluginId": "management", - "id": "def-public.ManagementApp.meta", - "type": "Object", + "id": "def-public.ManagementApp.keywords", + "type": "Array", "tags": [], - "label": "meta", + "label": "keywords", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppMeta", - "text": "AppMeta" - } + "string[]" ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 22 + "lineNumber": 21 }, "deprecated": false }, @@ -126,7 +120,7 @@ ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 24 + "lineNumber": 23 }, "deprecated": false, "children": [ @@ -148,7 +142,7 @@ ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 24 + "lineNumber": 23 }, "deprecated": false, "isRequired": true @@ -260,7 +254,7 @@ "section": "def-public.RegisterManagementAppArgs", "text": "RegisterManagementAppArgs" }, - ", \"title\" | \"id\" | \"meta\" | \"order\" | \"mount\" | \"euiIconType\" | \"icon\" | \"tip\">) => ", + ", \"title\" | \"id\" | \"order\" | \"mount\" | \"keywords\" | \"euiIconType\" | \"icon\" | \"tip\">) => ", { "pluginId": "management", "scope": "public", @@ -291,7 +285,7 @@ "section": "def-public.RegisterManagementAppArgs", "text": "RegisterManagementAppArgs" }, - ", \"title\" | \"id\" | \"meta\" | \"order\" | \"mount\" | \"euiIconType\" | \"icon\" | \"tip\">" + ", \"title\" | \"id\" | \"order\" | \"mount\" | \"keywords\" | \"euiIconType\" | \"icon\" | \"tip\">" ], "source": { "path": "src/plugins/management/public/utils/management_section.ts", @@ -653,7 +647,7 @@ ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 13 + "lineNumber": 12 }, "deprecated": false, "children": [ @@ -681,7 +675,7 @@ ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 14 + "lineNumber": 13 }, "deprecated": false, "returnComment": [], @@ -719,30 +713,23 @@ "description": [], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 15 + "lineNumber": 14 }, "deprecated": false }, { "parentPluginId": "management", - "id": "def-public.RegisterManagementAppArgs.meta", - "type": "Object", + "id": "def-public.RegisterManagementAppArgs.keywords", + "type": "Array", "tags": [], - "label": "meta", + "label": "keywords", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "public", - "docId": "kibCoreApplicationPluginApi", - "section": "def-public.AppMeta", - "text": "AppMeta" - }, - " | undefined" + "string[] | undefined" ], "source": { "path": "src/plugins/management/public/utils/management_app.ts", - "lineNumber": 16 + "lineNumber": 15 }, "deprecated": false } diff --git a/api_docs/ml.json b/api_docs/ml.json index 51a5608578026a..dee66858b9be94 100644 --- a/api_docs/ml.json +++ b/api_docs/ml.json @@ -1320,7 +1320,7 @@ }, " extends Pick<", "UseDataGridReturnType", - ", \"status\" | \"errorMessage\" | \"baseline\" | \"pagination\" | \"chartsVisible\" | \"chartsButtonVisible\" | \"ccsWarning\" | \"columnsWithCharts\" | \"invalidSortingColumnns\" | \"noDataMessage\" | \"onChangeItemsPerPage\" | \"onChangePage\" | \"onSort\" | \"setPagination\" | \"setVisibleColumns\" | \"rowCount\" | \"rowCountRelation\" | \"sortingColumns\" | \"tableItems\" | \"toggleChartVisibility\" | \"visibleColumns\" | \"predictionFieldName\" | \"resultsField\">" + ", \"status\" | \"errorMessage\" | \"baseline\" | \"chartsVisible\" | \"chartsButtonVisible\" | \"ccsWarning\" | \"columnsWithCharts\" | \"invalidSortingColumnns\" | \"noDataMessage\" | \"onChangeItemsPerPage\" | \"onChangePage\" | \"onSort\" | \"pagination\" | \"setPagination\" | \"setVisibleColumns\" | \"rowCount\" | \"rowCountRelation\" | \"sortingColumns\" | \"tableItems\" | \"toggleChartVisibility\" | \"visibleColumns\" | \"predictionFieldName\" | \"resultsField\">" ], "source": { "path": "x-pack/plugins/ml/public/application/components/data_grid/types.ts", diff --git a/api_docs/saved_objects.json b/api_docs/saved_objects.json index 566e0e9a14ae99..48579ca8e589e0 100644 --- a/api_docs/saved_objects.json +++ b/api_docs/saved_objects.json @@ -680,20 +680,6 @@ }, "deprecated": true, "references": [ - { - "plugin": "visualizations", - "link": { - "path": "src/plugins/visualizations/public/saved_visualizations/find_list_items.ts", - "lineNumber": 16 - } - }, - { - "plugin": "visualizations", - "link": { - "path": "src/plugins/visualizations/public/saved_visualizations/find_list_items.ts", - "lineNumber": 35 - } - }, { "plugin": "discover", "link": { @@ -722,6 +708,20 @@ "lineNumber": 88 } }, + { + "plugin": "visualizations", + "link": { + "path": "src/plugins/visualizations/public/saved_visualizations/find_list_items.ts", + "lineNumber": 16 + } + }, + { + "plugin": "visualizations", + "link": { + "path": "src/plugins/visualizations/public/saved_visualizations/find_list_items.ts", + "lineNumber": 35 + } + }, { "plugin": "visualizations", "link": { @@ -2569,31 +2569,31 @@ } }, { - "plugin": "visualizations", + "plugin": "discover", "link": { - "path": "src/plugins/visualizations/public/types.ts", + "path": "src/plugins/discover/public/saved_searches/_saved_search.ts", "lineNumber": 9 } }, { - "plugin": "visualizations", + "plugin": "discover", "link": { - "path": "src/plugins/visualizations/public/types.ts", - "lineNumber": 41 + "path": "src/plugins/discover/public/saved_searches/_saved_search.ts", + "lineNumber": 61 } }, { - "plugin": "discover", + "plugin": "visualizations", "link": { - "path": "src/plugins/discover/public/saved_searches/_saved_search.ts", + "path": "src/plugins/visualizations/public/types.ts", "lineNumber": 9 } }, { - "plugin": "discover", + "plugin": "visualizations", "link": { - "path": "src/plugins/discover/public/saved_searches/_saved_search.ts", - "lineNumber": 61 + "path": "src/plugins/visualizations/public/types.ts", + "lineNumber": 41 } }, { diff --git a/api_docs/security.json b/api_docs/security.json index 6cc84bc75f0090..7e28ad2a222bb3 100644 --- a/api_docs/security.json +++ b/api_docs/security.json @@ -1306,13 +1306,6 @@ "lineNumber": 444 } }, - { - "plugin": "cases", - "link": { - "path": "x-pack/plugins/cases/server/plugin.ts", - "lineNumber": 89 - } - }, { "plugin": "ml", "link": { @@ -1321,52 +1314,52 @@ } }, { - "plugin": "securitySolution", + "plugin": "cases", "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts", - "lineNumber": 48 + "path": "x-pack/plugins/cases/server/plugin.ts", + "lineNumber": 89 } }, { - "plugin": "securitySolution", + "plugin": "dashboardMode", "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts", - "lineNumber": 45 + "path": "x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts", + "lineNumber": 33 } }, { - "plugin": "securitySolution", + "plugin": "dataEnhanced", "link": { - "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts", - "lineNumber": 45 + "path": "x-pack/plugins/data_enhanced/server/search/session/session_service.ts", + "lineNumber": 448 } }, { - "plugin": "beatsManagement", + "plugin": "logstash", "link": { - "path": "x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts", - "lineNumber": 44 + "path": "x-pack/plugins/logstash/server/routes/pipeline/save.ts", + "lineNumber": 41 } }, { - "plugin": "dashboardMode", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/dashboard_mode/server/interceptors/dashboard_mode_request_interceptor.ts", - "lineNumber": 33 + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/create_signals_migration_route.ts", + "lineNumber": 48 } }, { - "plugin": "dataEnhanced", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/data_enhanced/server/search/session/session_service.ts", - "lineNumber": 448 + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/delete_signals_migration_route.ts", + "lineNumber": 45 } }, { - "plugin": "logstash", + "plugin": "securitySolution", "link": { - "path": "x-pack/plugins/logstash/server/routes/pipeline/save.ts", - "lineNumber": 41 + "path": "x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/finalize_signals_migration_route.ts", + "lineNumber": 45 } }, { diff --git a/api_docs/spaces.json b/api_docs/spaces.json index aa4e651b9477ba..5225e8cebbeb5a 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -1213,13 +1213,6 @@ "lineNumber": 271 } }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 194 - } - }, { "plugin": "apm", "link": { @@ -1248,6 +1241,13 @@ "lineNumber": 153 } }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/plugin.ts", + "lineNumber": 194 + } + }, { "plugin": "security", "link": { @@ -2047,13 +2047,6 @@ "lineNumber": 303 } }, - { - "plugin": "securitySolution", - "link": { - "path": "x-pack/plugins/security_solution/server/plugin.ts", - "lineNumber": 194 - } - }, { "plugin": "apm", "link": { @@ -2088,6 +2081,13 @@ "path": "x-pack/plugins/infra/server/plugin.ts", "lineNumber": 153 } + }, + { + "plugin": "securitySolution", + "link": { + "path": "x-pack/plugins/security_solution/server/plugin.ts", + "lineNumber": 194 + } } ] }, diff --git a/docs/api/index-patterns.asciidoc b/docs/api/index-patterns.asciidoc new file mode 100644 index 00000000000000..79d2f164fb8c3b --- /dev/null +++ b/docs/api/index-patterns.asciidoc @@ -0,0 +1,30 @@ +[[index-patterns-api]] +== Index patterns APIs + +experimental[] Manage {kib} index patterns. + +WARNING: Do not write documents directly to the `.kibana` index. When you write directly +to the `.kibana` index, the data becomes corrupted and permanently breaks future {kib} versions. + +WARNING: Use the index patterns API for managing {kib} index patterns instead of lower-level <>. + +The following index patterns APIs are available: + +* Index patterns + ** <> to retrieve a single {kib} index pattern + ** <> to create {kib} index pattern + ** <> to partially updated {kib} index pattern + ** <> to delete {kib} index pattern +* Default index pattern + ** <> to retrieve a default index pattern + ** <> to set a default index pattern +* Fields + ** <> to change field metadata, such as `count`, `customLabel` and `format` + +include::index-patterns/get.asciidoc[] +include::index-patterns/create.asciidoc[] +include::index-patterns/update.asciidoc[] +include::index-patterns/delete.asciidoc[] +include::index-patterns/default-get.asciidoc[] +include::index-patterns/default-set.asciidoc[] +include::index-patterns/update-fields.asciidoc[] diff --git a/docs/api/index-patterns/create.asciidoc b/docs/api/index-patterns/create.asciidoc new file mode 100644 index 00000000000000..771292d6f934d5 --- /dev/null +++ b/docs/api/index-patterns/create.asciidoc @@ -0,0 +1,102 @@ +[[index-patterns-api-create]] +=== Create index pattern API +++++ +Create index pattern +++++ + +experimental[] Create {kib} index patterns. + +[[index-patterns-api-create-request]] +==== Request + +`POST :/api/index_patterns/index_pattern` + +`POST :/s//api/index_patterns/index_pattern` + +[[index-patterns-api-create-path-params]] +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[index-patterns-api-create-body-params]] +==== Request body + +`override`:: (Optional, boolean) Overrides an existing index pattern if an +index pattern with the provided title already exists. The default is `false`. + +`refresh_fields`:: (Optional, boolean) Reloads index pattern fields after +the index pattern is stored. The default is `false`. + +`index_pattern`:: (Required, object) The index pattern object. All fields are optional. + +[[index-patterns-api-create-request-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[index-patterns-api-create-example]] +==== Examples + +Create an index pattern with a custom title: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/index_pattern +{ + "index_pattern": { + "title": "hello" + } +} +-------------------------------------------------- +// KIBANA + +Customize the creation behavior: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/index_pattern +{ + "override": false, + "refresh_fields": true, + "index_pattern": { + "title": "hello" + } +} +-------------------------------------------------- +// KIBANA + +At creation, all index pattern fields are optional: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/index_pattern +{ + "index_pattern": { + "id": "...", + "version": "...", + "title": "...", + "type": "...", + "timeFieldName": "...", + "sourceFilters": [], + "fields": {}, + "typeMeta": {}, + "fieldFormats": {}, + "fieldAttrs": {}, + "allowNoIndex": "..." + } +} +-------------------------------------------------- +// KIBANA + + +The API returns the index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": {...} +} +-------------------------------------------------- + diff --git a/docs/api/index-patterns/default-get.asciidoc b/docs/api/index-patterns/default-get.asciidoc new file mode 100644 index 00000000000000..5c7e8e01ce3990 --- /dev/null +++ b/docs/api/index-patterns/default-get.asciidoc @@ -0,0 +1,55 @@ +[[index-patterns-api-default-get]] +=== Get default index pattern API +++++ +Get default index pattern +++++ + +experimental[] Retrieve a default index pattern ID. Kibana UI uses default index pattern unless user picks a different one. + +[[index-patterns-api-default-get-request]] +==== Request + +`GET :/api/index_patterns/default` + +`GET :/s//api/index_patterns/default` + +[[index-patterns-api-default-get-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[index-patterns-api-default-get-codes]] +==== Response code + +`200`:: +Indicates a successful call. + +[[index-patterns-api-default-get-example]] +==== Example + +Retrieve the default index pattern id: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/index_patterns/default +-------------------------------------------------- +// KIBANA + +The API returns an ID of a default index pattern: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern_id": "..." +} +-------------------------------------------------- + +In case there is no default index pattern, the API returns: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern_id": null +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/default-set.asciidoc b/docs/api/index-patterns/default-set.asciidoc new file mode 100644 index 00000000000000..3b3cec6be6324b --- /dev/null +++ b/docs/api/index-patterns/default-set.asciidoc @@ -0,0 +1,84 @@ +[[index-patterns-api-default-set]] +=== Set default index pattern API +++++ +Set default index pattern +++++ + +experimental[] Set a default index pattern ID. Kibana UI will use default index pattern unless user picks a different one. +The API doesn't validate if given `index_pattern_id` is a valid id. + +[[index-patterns-api-default-set-request]] +==== Request + +`POST :/api/index_patterns/default` + +`POST :/s//api/index_patterns/default` + +[[index-patterns-api-default-set-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +[[index-patterns-api-default-set-body]] +==== Request body + +`index_pattern_id`:: (Required, `string` or `null`) Sets a default index pattern id. Use `null` to unset a default index pattern. + +`force`:: (Optional, boolean) Updates existing default index pattern id. The default is `false`. + + +[[index-patterns-api-default-set-codes]] +==== Response code + +`200`:: +Indicates a successful call. + +[[index-patterns-api-default-set-example]] +==== Example + +Set the default index pattern id if none is set: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/default +{ + "index_pattern_id": "..." +} +-------------------------------------------------- +// KIBANA + + +Upsert the default index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/default +{ + "index_pattern_id": "...", + "force": true +} +-------------------------------------------------- +// KIBANA + +Unset the default index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/index_patterns/default +{ + "index_pattern_id": null, + "force": true +} +-------------------------------------------------- +// KIBANA + +The API returns: + +[source,sh] +-------------------------------------------------- +{ + "acknowledged": true +} +-------------------------------------------------- + diff --git a/docs/api/index-patterns/delete.asciidoc b/docs/api/index-patterns/delete.asciidoc new file mode 100644 index 00000000000000..a25f2f13e0b41f --- /dev/null +++ b/docs/api/index-patterns/delete.asciidoc @@ -0,0 +1,41 @@ +[[index-patterns-api-delete]] +=== Delete index pattern API +++++ +Delete index pattern +++++ + +experimental[] Delete {kib} index patterns. + +WARNING: Once you delete an index pattern, _it cannot be recovered_. + +[[index-patterns-api-delete-request]] +==== Request + +`DELETE :/api/index_patterns/index_pattern/` + +`DELETE :/s//api/index_patterns/index_pattern/` + +[[index-patterns-api-delete-path-params]] +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`id`:: + (Required, string) The ID of the index pattern you want to delete. + +[[index-patterns-api-delete-response-codes]] +==== Response code + +`200`:: + Indicates that index pattern is deleted. Returns an empty response body. + +==== Example + +Delete an index pattern object with the `my-pattern` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/index_patterns/index_pattern/my-pattern +-------------------------------------------------- +// KIBANA diff --git a/docs/api/index-patterns/get.asciidoc b/docs/api/index-patterns/get.asciidoc new file mode 100644 index 00000000000000..3f53bf0726bf14 --- /dev/null +++ b/docs/api/index-patterns/get.asciidoc @@ -0,0 +1,64 @@ +[[index-patterns-api-get]] +=== Get index pattern API +++++ +Get index pattern +++++ + +experimental[] Retrieve a single {kib} index pattern by ID. + +[[index-patterns-api-get-request]] +==== Request + +`GET :/api/index_patterns/index_pattern/` + +`GET :/s//api/index_patterns/index_pattern/` + +[[index-patterns-api-get-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`id`:: +(Required, string) The ID of the index pattern you want to retrieve. + +[[index-patterns-api-get-codes]] +==== Response code + +`200`:: +Indicates a successful call. + +`404`:: +The specified index pattern and ID doesn't exist. + +[[index-patterns-api-get-example]] +==== Example + +Retrieve the index pattern object with the `my-pattern` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/index_patterns/index_pattern/my-pattern +-------------------------------------------------- +// KIBANA + +The API returns an index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": { + "id": "my-pattern", + "version": "...", + "title": "...", + "type": "...", + "timeFieldName": "...", + "sourceFilters": [], + "fields": {}, + "typeMeta": {}, + "fieldFormats": {}, + "fieldAttrs": {}, + "allowNoIndex: "..." + } +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/update-fields.asciidoc b/docs/api/index-patterns/update-fields.asciidoc new file mode 100644 index 00000000000000..e3f98631bb52a3 --- /dev/null +++ b/docs/api/index-patterns/update-fields.asciidoc @@ -0,0 +1,100 @@ +[[index-patterns-fields-api-update]] +=== Update index pattern fields API +++++ +Update index pattern fields metadata +++++ + +experimental[] Update fields presentation metadata, such as `count`, +`customLabel`, and `format`. You can update multiple fields in one request. Updates +are merged with persisted metadata. To remove existing metadata, specify `null` as the value. + +[[index-patterns-fields-api-update-request]] +==== Request + +`POST :/api/index_patterns/index_pattern//fields` + +`POST :/s//api/index_patterns/index_pattern//fields` + +[[index-patterns-fields-api-update-path-params]] +==== Path parameters + +`space_id`:: +(Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`id`:: +(Required, string) The ID of the index pattern fields you want to update. + +[[index-patterns-fields-api-update-request-body]] +==== Request body + +`fields`:: +(Required, object) the field object + + +[[index-patterns-fields-api-update-errors-codes]] +==== Response code + +`200`:: +Indicates a successful call. + +[[index-patterns-fields-api-update-example]] +==== Examples + +Set popularity `count` for field `foo`: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/saved_objects/index-pattern/my-pattern/fields +{ + "fields": { + "foo": { + "count": 123 + } + } +} +-------------------------------------------------- +// KIBANA + +Update multiple metadata fields in one request: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/saved_objects/index-pattern/my-pattern/fields +{ + "fields": { + "foo": { + "count": 123, + "customLabel": "Foo" + }, + "bar": { + "customLabel": "Bar" + } + } +} +-------------------------------------------------- +// KIBANA + +Use `null` value to delete metadata: +[source,sh] +-------------------------------------------------- +$ curl -X POST api/saved_objects/index-pattern/my-pattern/fields +{ + "fields": { + "foo": { + "customLabel": null + } + } +} +-------------------------------------------------- +// KIBANA + + +The endpoint returns the updated index pattern object: +[source,sh] +-------------------------------------------------- +{ + "index_pattern": { + + } +} +-------------------------------------------------- diff --git a/docs/api/index-patterns/update.asciidoc b/docs/api/index-patterns/update.asciidoc new file mode 100644 index 00000000000000..8ed0ff89fb928c --- /dev/null +++ b/docs/api/index-patterns/update.asciidoc @@ -0,0 +1,111 @@ +[[index-patterns-api-update]] +=== Update index pattern API +++++ +Update index pattern +++++ + +experimental[] Update part of an index pattern. Only the specified fields are updated in the +index pattern. Unspecified fields stay as they are persisted. + +[[index-patterns-api-update-request]] +==== Request + +`POST :/api/index_patterns/index_pattern/` + +`POST :/s//api/index_patterns/index_pattern/` + +[[index-patterns-api-update-path-params]] +==== Path parameters + +`space_id`:: + (Optional, string) An identifier for the space. If `space_id` is not provided in the URL, the default space is used. + +`id`:: + (Required, string) The ID of the index pattern you want to update. + +[[index-patterns-api-update-request-body]] +==== Request body + +`refresh_fields`:: (Optional, boolean) Reloads the index pattern fields after +the index pattern is updated. The default is `false`. + +`index_pattern`:: + (Required, object) The index patterns fields you want to update. ++ + +You can partially update the following fields: + +* `title` +* `timeFieldName` +* `fields` +* `sourceFilters` +* `fieldFormatMap` +* `type` +* `typeMeta` + +[[index-patterns-api-update-errors-codes]] +==== Response code + +`200`:: + Indicates a successful call. + +[[index-patterns-api-update-example]] +==== Examples + +Update a title of the `` index pattern: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/saved_objects/index-pattern/my-pattern +{ + "index_pattern": { + "title": "some-other-pattern-*" + } +} +-------------------------------------------------- +// KIBANA + +Customize the update behavior: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/saved_objects/index-pattern/my-pattern +{ + "refresh_fields": true, + "index_pattern": { + "fields": {} + } +} +-------------------------------------------------- +// KIBANA + + +All update fields are optional, but you can specify the following fields: + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/saved_objects/index-pattern/my-pattern +{ + "index_pattern": { + "title": "...", + "timeFieldName": "...", + "sourceFilters": [], + "fieldFormats": {}, + "type": "...", + "typeMeta": {}, + "fields": {} + } +} +-------------------------------------------------- +// KIBANA + +The API returns the updated index pattern object: + +[source,sh] +-------------------------------------------------- +{ + "index_pattern": { + + } +} +-------------------------------------------------- diff --git a/docs/api/saved-objects.asciidoc b/docs/api/saved-objects.asciidoc index ecf975134c64a7..ba4e5a7e656fc8 100644 --- a/docs/api/saved-objects.asciidoc +++ b/docs/api/saved-objects.asciidoc @@ -1,11 +1,13 @@ [[saved-objects-api]] == Saved objects APIs -Manage {kib} saved objects, including dashboards, visualizations, index patterns, and more. +Manage {kib} saved objects, including dashboards, visualizations, and more. WARNING: Do not write documents directly to the `.kibana` index. When you write directly to the `.kibana` index, the data becomes corrupted and permanently breaks future {kib} versions. +NOTE: For managing {kib} index patterns, use the <>. + The following saved objects APIs are available: * <> to retrieve a single {kib} saved object by ID diff --git a/docs/developer/contributing/development-package-tests.asciidoc b/docs/developer/contributing/development-package-tests.asciidoc new file mode 100644 index 00000000000000..10c09d6cae8c07 --- /dev/null +++ b/docs/developer/contributing/development-package-tests.asciidoc @@ -0,0 +1,64 @@ +[[development-package-tests]] +== Package Testing + +Packaging tests use Vagrant virtual machines as hosts and Ansible for +provisioning and assertions. Kibana distributions are copied from the +target folder into each VM and installed, along with required +dependencies. + +=== Setup + +* https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html[Ansible] ++ +``` +# Ubuntu +sudo apt-get install python3-pip libarchive-tools +pip3 install --user ansible + +# Darwin +brew install python3 +pip3 install --user ansible +``` +* https://www.vagrantup.com/downloads[Vagrant] +* https://www.virtualbox.org/wiki/Downloads[Virtualbox] + +=== Machines + +[cols=",,",options="header",] +|=== +|Hostname |IP |Description +|deb |192.168.50.5 |Installation of Kibana’s deb package +|rpm |192.168.50.6 |Installation of Kibana’s rpm package +|docker |192.168.50.7 |Installation of Kibana’s docker image +|=== + +=== Running + +``` +# Build distributions +node scripts/build --all-platforms --debug --no-oss + +cd test/package + +# Setup virtual machine and networking +vagrant up --no-provision + +# Install Kibana and run OS level tests +# This step can be repeated when adding new tests, it ensures machine state - installations won't run twice +vagrant provision + +# Running functional tests +node scripts/es snapshot \ + -E network.bind_host=127.0.0.1,192.168.50.1 \ + -E discovery.type=single-node \ + --license=trial +TEST_KIBANA_URL=http://elastic:changeme@:5601 \ +TEST_ES_URL=http://elastic:changeme@192.168.50.1:9200 \ + node scripts/functional_test_runner.js --include-tag=smoke +``` + +=== Cleanup + +.... +vagrant destroy +.... diff --git a/docs/developer/contributing/development-tests.asciidoc b/docs/developer/contributing/development-tests.asciidoc index 715b1a15ab5ede..e7a36d28667282 100644 --- a/docs/developer/contributing/development-tests.asciidoc +++ b/docs/developer/contributing/development-tests.asciidoc @@ -74,6 +74,7 @@ to learn more about using the node scripts we provide for building * <> * <> * <> +* <> include::development-functional-tests.asciidoc[leveloffset=+1] @@ -81,6 +82,8 @@ include::development-unit-tests.asciidoc[leveloffset=+1] include::development-accessibility-tests.asciidoc[leveloffset=+1] +include::development-package-tests.asciidoc[leveloffset=+1] + [discrete] === Cross-browser compatibility diff --git a/docs/developer/getting-started/monorepo-packages.asciidoc b/docs/developer/getting-started/monorepo-packages.asciidoc index 8f033029cfac42..4e8bbf76eaacb0 100644 --- a/docs/developer/getting-started/monorepo-packages.asciidoc +++ b/docs/developer/getting-started/monorepo-packages.asciidoc @@ -82,10 +82,12 @@ yarn kbn watch-bazel - @kbn/i18n - @kbn/legacy-logging - @kbn/logging +- @kbn/mapbox-gl +- @kbn/monaco - @kbn/securitysolution-es-utils -- kbn/securitysolution-io-ts-alerting-types -- kbn/securitysolution-io-ts-list-types -- kbn/securitysolution-io-ts-types +- @kbn/securitysolution-io-ts-alerting-types +- @kbn/securitysolution-io-ts-list-types +- @kbn/securitysolution-io-ts-types - @kbn/securitysolution-io-ts-utils - @kbn/securitysolution-list-api - @kbn/securitysolution-list-constants diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 94384024e09357..087626240ff337 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -332,11 +332,6 @@ which will load the visualization's editor. |Allow to add a header banner that will be displayed on every page of the Kibana application -|{kib-repo}blob/{branch}/x-pack/plugins/beats_management/readme.md[beatsManagement] -|Notes: -Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place - - |{kib-repo}blob/{branch}/x-pack/plugins/canvas/README.md[canvas] |"Never look back. The past is done. The future is a blank canvas." ― Suzy Kassem, Rise Up and Salute the Sun diff --git a/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md index 0392cb7eaefb02..8186996b63fe55 100644 --- a/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md +++ b/docs/development/core/public/kibana-plugin-core-public.app.deeplinks.md @@ -23,14 +23,24 @@ core.application.register({ title: 'Translated title', keywords: ['translated keyword1', 'translated keyword2'], deepLinks: [ - { id: 'sub1', title: 'Sub1', path: '/sub1', keywords: ['subpath1'] }, - { - id: 'sub2', - title: 'Sub2', - deepLinks: [ - { id: 'subsub', title: 'SubSub', path: '/sub2/sub', keywords: ['subpath2'] } - ] - } + { + id: 'sub1', + title: 'Sub1', + path: '/sub1', + keywords: ['subpath1'], + }, + { + id: 'sub2', + title: 'Sub2', + deepLinks: [ + { + id: 'subsub', + title: 'SubSub', + path: '/sub2/sub', + keywords: ['subpath2'], + }, + ], + }, ], mount: () => { ... } }) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 0448ad42c94fa9..9930ab7319f653 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -111,6 +111,7 @@ readonly links: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; + readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 78d2d8daa3d457..ab8cdea5e4d869 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
} | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md new file mode 100644 index 00000000000000..d9c47dec9e9d46 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) > [controlledBy](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md) + +## ApplyGlobalFilterActionContext.controlledBy property + +Signature: + +```typescript +controlledBy?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md index 2f844b6844645d..01ccd4819d9062 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md @@ -14,6 +14,7 @@ export interface ApplyGlobalFilterActionContext | Property | Type | Description | | --- | --- | --- | +| [controlledBy](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.controlledby.md) | string | | | [embeddable](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.embeddable.md) | unknown | | | [filters](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.filters.md) | Filter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.timefieldname.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md index 742b54e19216e0..54b5a33ccf6822 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.esfilters.md @@ -33,6 +33,7 @@ esFilters: { disabled: boolean; controlledBy?: string | undefined; index?: string | undefined; + isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefaultid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefaultid.md new file mode 100644 index 00000000000000..3b64ce079b5222 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.getdefaultid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPatternsService](./kibana-plugin-plugins-data-public.indexpatternsservice.md) > [getDefaultId](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefaultid.md) + +## IndexPatternsService.getDefaultId property + +Get default index pattern id + +Signature: + +```typescript +getDefaultId: () => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md index 1511de18cab513..26b393a5fb5b63 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.md @@ -27,6 +27,7 @@ export declare class IndexPatternsService | [get](./kibana-plugin-plugins-data-public.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-public.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getDefaultId](./kibana-plugin-plugins-data-public.indexpatternsservice.getdefaultid.md) | | () => Promise<string | null> | Get default index pattern id | | [getFieldsForIndexPattern](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | | [getFieldsForWildcard](./kibana-plugin-plugins-data-public.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-public.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | @@ -34,7 +35,7 @@ export declare class IndexPatternsService | [getTitles](./kibana-plugin-plugins-data-public.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | | [refreshFields](./kibana-plugin-plugins-data-public.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | | [savedObjectToSpec](./kibana-plugin-plugins-data-public.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | -| [setDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | +| [setDefault](./kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md) | | (id: string | null, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | ## Methods diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md index 2bf8eaa03d1ae9..1d216e781c7bb6 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpatternsservice.setdefault.md @@ -9,5 +9,5 @@ Optionally set default index pattern, unless force = true Signature: ```typescript -setDefault: (id: string, force?: boolean) => Promise; +setDefault: (id: string | null, force?: boolean) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefaultid.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefaultid.md new file mode 100644 index 00000000000000..107d1e4e94a0dd --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.getdefaultid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [IndexPatternsService](./kibana-plugin-plugins-data-server.indexpatternsservice.md) > [getDefaultId](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefaultid.md) + +## IndexPatternsService.getDefaultId property + +Get default index pattern id + +Signature: + +```typescript +getDefaultId: () => Promise; +``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md index d55a6e9b325a20..f5e845ced3cd13 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.md @@ -27,6 +27,7 @@ export declare class IndexPatternsService | [get](./kibana-plugin-plugins-data-server.indexpatternsservice.get.md) | | (id: string) => Promise<IndexPattern> | Get an index pattern by id. Cache optimized | | [getCache](./kibana-plugin-plugins-data-server.indexpatternsservice.getcache.md) | | () => Promise<SavedObject<IndexPatternSavedObjectAttrs>[] | null | undefined> | | | [getDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefault.md) | | () => Promise<IndexPattern | null> | Get default index pattern | +| [getDefaultId](./kibana-plugin-plugins-data-server.indexpatternsservice.getdefaultid.md) | | () => Promise<string | null> | Get default index pattern id | | [getFieldsForIndexPattern](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforindexpattern.md) | | (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise<any> | Get field list by providing an index patttern (or spec) | | [getFieldsForWildcard](./kibana-plugin-plugins-data-server.indexpatternsservice.getfieldsforwildcard.md) | | (options: GetFieldsOptions) => Promise<any> | Get field list by providing { pattern } | | [getIds](./kibana-plugin-plugins-data-server.indexpatternsservice.getids.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern ids | @@ -34,7 +35,7 @@ export declare class IndexPatternsService | [getTitles](./kibana-plugin-plugins-data-server.indexpatternsservice.gettitles.md) | | (refresh?: boolean) => Promise<string[]> | Get list of index pattern titles | | [refreshFields](./kibana-plugin-plugins-data-server.indexpatternsservice.refreshfields.md) | | (indexPattern: IndexPattern) => Promise<void> | Refresh field list for a given index pattern | | [savedObjectToSpec](./kibana-plugin-plugins-data-server.indexpatternsservice.savedobjecttospec.md) | | (savedObject: SavedObject<IndexPatternAttributes>) => IndexPatternSpec | Converts index pattern saved object to index pattern spec | -| [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) | | (id: string, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | +| [setDefault](./kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md) | | (id: string | null, force?: boolean) => Promise<void> | Optionally set default index pattern, unless force = true | ## Methods diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md index 708d645a79f1a7..6dc584341eef3e 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsservice.setdefault.md @@ -9,5 +9,5 @@ Optionally set default index pattern, unless force = true Signature: ```typescript -setDefault: (id: string, force?: boolean) => Promise; +setDefault: (id: string | null, force?: boolean) => Promise; ``` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.request.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.request.md new file mode 100644 index 00000000000000..18163bfebde7e2 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.searchstrategydependencies.request.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [SearchStrategyDependencies](./kibana-plugin-plugins-data-server.searchstrategydependencies.md) > [request](./kibana-plugin-plugins-data-server.searchstrategydependencies.request.md) + +## SearchStrategyDependencies.request property + +Signature: + +```typescript +request: KibanaRequest; +``` diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index ec5677bd04a6e2..65b600d4b7281f 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -71,6 +71,11 @@ image::images/connector-listing.png[Example connector listing in the Rules and C Access to connectors is granted based on your privileges to alerting-enabled features. See <> for more information. +[float] +=== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[connectors-list]] === Connector list @@ -110,7 +115,7 @@ image::images/connector-select-type.png[Connector select type] [[importing-and-exporting-connectors]] === Importing and exporting connectors -To import and export rules, use the <>. +To import and export connectors, use the <>. After a successful import, the proper banner is displayed: [role="screenshot"] image::images/coonectors-import-banner.png[Connectors import banner, width=50%] diff --git a/docs/management/connectors/action-types/email.asciidoc b/docs/management/connectors/action-types/email.asciidoc index 1c2f9212b48871..719d00c16c932a 100644 --- a/docs/management/connectors/action-types/email.asciidoc +++ b/docs/management/connectors/action-types/email.asciidoc @@ -24,6 +24,12 @@ Require authentication:: If true, a username and password for login type authent Username:: Username for login type authentication. Password:: Password for login type authentication. +[float] +[[email-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[preconfigured-email-configuration]] ==== Preconfigured connector type diff --git a/docs/management/connectors/action-types/jira.asciidoc b/docs/management/connectors/action-types/jira.asciidoc index a5e629887d5c6e..368b11225654c3 100644 --- a/docs/management/connectors/action-types/jira.asciidoc +++ b/docs/management/connectors/action-types/jira.asciidoc @@ -19,6 +19,12 @@ Project key:: Jira project key. Email (or username):: The account email (or username) for HTTP Basic authentication. API token (or password):: Jira API authentication token (or password) for HTTP Basic authentication. +[float] +[[jira-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[Preconfigured-jira-configuration]] ==== Preconfigured connector type diff --git a/docs/management/connectors/action-types/pagerduty.asciidoc b/docs/management/connectors/action-types/pagerduty.asciidoc index 25cba050105480..db1c4e3932d148 100644 --- a/docs/management/connectors/action-types/pagerduty.asciidoc +++ b/docs/management/connectors/action-types/pagerduty.asciidoc @@ -17,6 +17,12 @@ Name:: The name of the connector. The name is used to identify a connector API URL:: An optional PagerDuty event URL. Defaults to `https://events.pagerduty.com/v2/enqueue`. If you are using the <> setting, make sure the hostname is added to the allowed hosts. Integration Key:: A 32 character PagerDuty Integration Key for an integration on a service, also referred to as the routing key. +[float] +[[pagerduty-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[Preconfigured-pagerduty-configuration]] ==== Preconfigured connector type diff --git a/docs/management/connectors/action-types/resilient.asciidoc b/docs/management/connectors/action-types/resilient.asciidoc index 454ae145bbc579..ef8196bea0aabc 100644 --- a/docs/management/connectors/action-types/resilient.asciidoc +++ b/docs/management/connectors/action-types/resilient.asciidoc @@ -19,6 +19,12 @@ Organization ID:: IBM Resilient organization ID. API key ID:: The authentication key ID for HTTP Basic authentication. API key secret:: The authentication key secret for HTTP Basic authentication. +[float] +[[resilient-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[Preconfigured-resilient-configuration]] ==== Preconfigured connector type diff --git a/docs/management/connectors/action-types/servicenow.asciidoc b/docs/management/connectors/action-types/servicenow.asciidoc index 24892c62e804b2..dfac22cb23c6c3 100644 --- a/docs/management/connectors/action-types/servicenow.asciidoc +++ b/docs/management/connectors/action-types/servicenow.asciidoc @@ -18,6 +18,12 @@ URL:: ServiceNow instance URL. Username:: Username for HTTP Basic authentication. Password:: Password for HTTP Basic authentication. +[float] +[[servicenow-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[Preconfigured-servicenow-configuration]] ==== Preconfigured connector type diff --git a/docs/management/connectors/action-types/slack.asciidoc b/docs/management/connectors/action-types/slack.asciidoc index da0bf321f9ade7..6dffebd9d9354e 100644 --- a/docs/management/connectors/action-types/slack.asciidoc +++ b/docs/management/connectors/action-types/slack.asciidoc @@ -16,6 +16,12 @@ Slack connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messaging/webhooks#getting_started[Slack Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is added to the allowed hosts. +[float] +[[slack-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[Preconfigured-slack-configuration]] ==== Preconfigured connector type diff --git a/docs/management/connectors/action-types/teams.asciidoc b/docs/management/connectors/action-types/teams.asciidoc index ba723a6f33c860..32cfaaf801d70e 100644 --- a/docs/management/connectors/action-types/teams.asciidoc +++ b/docs/management/connectors/action-types/teams.asciidoc @@ -16,6 +16,12 @@ Microsoft Teams connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the management UI connector listing, or in the connector list when configuring an action. Webhook URL:: The URL of the incoming webhook. See https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook#add-an-incoming-webhook-to-a-teams-channel[Add Incoming Webhooks] for instructions on generating this URL. If you are using the <> setting, make sure the hostname is added to the allowed hosts. +[float] +[[teams-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[Preconfigured-teams-configuration]] ==== Preconfigured connector type diff --git a/docs/management/connectors/action-types/webhook.asciidoc b/docs/management/connectors/action-types/webhook.asciidoc index a2024b9457a1c5..aa52e8a3bdb433 100644 --- a/docs/management/connectors/action-types/webhook.asciidoc +++ b/docs/management/connectors/action-types/webhook.asciidoc @@ -21,6 +21,12 @@ Require authentication:: If true, a username and password for login type authent Username:: Username for HTTP basic authentication. Password:: Password for HTTP basic authentication. +[float] +[[webhook-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + [float] [[Preconfigured-webhook-configuration]] ==== Preconfigured connector type diff --git a/docs/management/managing-beats.asciidoc b/docs/management/managing-beats.asciidoc deleted file mode 100644 index 232efb60cadd3e..00000000000000 --- a/docs/management/managing-beats.asciidoc +++ /dev/null @@ -1,108 +0,0 @@ -[[managing-beats]] -[role="xpack"] -== {beats} Central Management - -include::{asciidoc-dir}/../../shared/discontinued.asciidoc[tag=cm-discontinued] - -To use {beats} Central Management, open the main menu, click *Stack Management > -{beats} Central Management*, then define and -manage configurations in a central location in {kib} and quickly deploy -configuration changes to all {beats} running across your enterprise. For more -about central management, see the related {beats} documentation: - -* {filebeat-ref}/configuration-central-management.html[{filebeat} documentation] -* {metricbeat-ref}/configuration-central-management.html[{metricbeat} documentation] - -[NOTE] -==== -This feature requires an Elastic license that includes {beats} central -management. - -Don't have a license? You can start a 30-day trial. Open the main menu, then -click *Stack Management > License Management*. At the end of the trial -period, you can purchase a subscription to keep using central management. For -more information, see https://www.elastic.co/subscriptions and -<>. -==== - -{kib} makes it easy for you to use central management by walking you through the -enrollment and configuration process step by step the first time you use the -Central Management UI. - -[float] -=== Required permissions - -You must have the `beats_admin` role assigned to use **{beats} Central Management** - -To assign the role, open the menu, then click *Stack Management > Users*. - - -[float] -=== Enroll {beats} - -You need to enroll {beats} to register them in central management and establish -trust. Enrolled {beats} will have the credentials needed to retrieve -configurations from {kib}. - -[float] -=== Create configuration tags - -A _configuration tag_ is a group of configuration blocks that you can apply to -one or more {beats}. For example, you can create a tag called `development` to -group configurations for {beats} running in your development environment. - -The first time you walk through the enrollment process, you'll create a -configuration tag that's applied to the {beats} instance you're enrolling. - -After that, under *Configuration tags*, you can create additional tags and -apply them to any enrolled {beats}, and the {beats} will use the configurations -defined in the tag. - -[float] -=== Add configuration blocks - -Add one or more configuration blocks to the tag. A tag can have configuration -blocks for different types of {beats}. When the enrolled {beats} run, they will -use the configuration blocks that are valid for their type. - -Central management supports configuration settings for: - -* {filebeat} modules -* {metricbeat} modules -* {filebeat} inputs -* {filebeat} and {metricbeat} outputs - -NOTE: Central management supports the following outputs only: {es}, {ls}, Kafka, -and Redis. Other output types are not supported for {beats} that are enrolled in -central management. - -Use the Central Management UI to define and manage settings for supported -configuration blocks. You cannot define those settings in local {beats} -configuration files. For configuration blocks that are not supported by central -management, configure the settings in the local configuration file after -enrolling the Beat in central management. - -[float] -=== Manage enrolled {beats} - -Under *Enrolled {beats}*, you can view the list of enrolled {beats} to see -details, including the type, applied tags, configuration status, and the last -configuration update. Click the *Beat name* or *Type* column heading to sort the -list. To filter the list, enter a search string. If there are errors in a -configuration, you’ll see an Error status in the Central Management UI and need -to look at {beats} logs to troubleshoot the problem. - -You can add or remove tags, and the configuration changes are automatically -deployed to all {beats} that have the tag. Avoid applying tags with conflicting -configurations. Because the configurations for all assigned tags are merged, -conflicting configurations result in errors. - -You can unenroll {beats} to remove them from central management. - -[float] -=== Manage tags - -Under *Configuration tags*, you can select tags and delete them, or you can -drill down into a tag to add, modify, or remove configuration blocks from the -tag. When you change the configuration blocks or remove tags, the configuration -changes are automatically deployed to all {beats} that have the tag. diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 50ed0d2652c6f0..71f141d1ed5d6e 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -41,7 +41,7 @@ You can configure the following settings in the `kibana.yml` file. [cols="2*<"] |=== | `xpack.actions.enabled` - | Feature toggle that enables Actions in {kib}. Defaults to `true`. + | Feature toggle that enables Actions in {kib}. Default: `true`. | `xpack.actions.allowedHosts` {ess-icon} | A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections. + @@ -50,7 +50,7 @@ You can configure the following settings in the `kibana.yml` file. | `xpack.actions.customHostSettings` {ess-icon} | A list of custom host settings to override existing global settings. - Defaults to an empty list. + + Default: an empty list. + + Each entry in the list must have a `url` property, to associate a connection type (mail or https), hostname and port with the remaining options in the @@ -70,6 +70,7 @@ You can configure the following settings in the `kibana.yml` file. xpack.actions.customHostSettings: - url: smtp://mail.example.com:465 tls: + verificationMode: 'full' certificateAuthoritiesFiles: [ 'one.crt' ] certificateAuthoritiesData: | -----BEGIN CERTIFICATE----- @@ -79,7 +80,9 @@ xpack.actions.customHostSettings: requireTLS: true - url: https://webhook.example.com tls: + // legacy rejectUnauthorized: false + verificationMode: 'none' -- [cols="2*<"] @@ -115,10 +118,16 @@ xpack.actions.customHostSettings: | `xpack.actions.customHostSettings[n]` `.tls.rejectUnauthorized` {ess-icon} - | A boolean value indicating whether to bypass server certificate validation. + | Deprecated. Use <> instead. A boolean value indicating whether to bypass server certificate validation. Overrides the general `xpack.actions.rejectUnauthorized` configuration for requests made for this hostname/port. +|[[action-config-custom-host-verification-mode]] `xpack.actions.customHostSettings[n]` +`.tls.verificationMode` + | Controls the verification of the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the host server. Valid values are `full`, `certificate`, and `none`. + Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. Overrides the general `xpack.actions.tls.verificationMode` configuration + for requests made for this hostname/port. + | `xpack.actions.customHostSettings[n]` `.tls.certificateAuthoritiesFiles` | A file name or list of file names of PEM-encoded certificate files to use @@ -137,10 +146,10 @@ xpack.actions.customHostSettings: | `xpack.actions` `.preconfiguredAlertHistoryEsIndex` {ess-icon} - | Enables a preconfigured alert history {es} <> connector. Defaults to `false`. + | Enables a preconfigured alert history {es} <> connector. Default: `false`. | `xpack.actions.preconfigured` - | Specifies preconfigured connector IDs and configs. Defaults to {}. + | Specifies preconfigured connector IDs and configs. Default: {}. | `xpack.actions.proxyUrl` {ess-icon} | Specifies the proxy URL to use, if using a proxy for actions. By default, no proxy is used. @@ -152,27 +161,44 @@ xpack.actions.customHostSettings: | Specifies hostnames which should only use the proxy, if using a proxy for actions. The value is an array of hostnames as strings. By default, no hosts will use the proxy, but if an action's hostname is in this list, the proxy will be used. The settings `xpack.actions.proxyBypassHosts` and `xpack.actions.proxyOnlyHosts` cannot be used at the same time. | `xpack.actions.proxyHeaders` {ess-icon} - | Specifies HTTP headers for the proxy, if using a proxy for actions. Defaults to {}. + | Specifies HTTP headers for the proxy, if using a proxy for actions. Default: {}. a|`xpack.actions.` `proxyRejectUnauthorizedCertificates` {ess-icon} - | Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Defaults to `true`. + | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for the proxy, if using a proxy for actions. Default: `true`. + +|[[action-config-proxy-verification-mode]] +`xpack.actions[n]` +`.tls.proxyVerificationMode` {ess-icon} +| Controls the verification for the proxy server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection to the proxy server. Valid values are `full`, `certificate`, and `none`. +Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. | `xpack.actions.rejectUnauthorized` {ess-icon} - | Set to `false` to bypass certificate validation for actions. Defaults to `true`. + + | Deprecated. Use <> instead. Set to `false` to bypass certificate validation for actions. Default: `true`. + + As an alternative to setting `xpack.actions.rejectUnauthorized`, you can use the setting `xpack.actions.customHostSettings` to set TLS options for specific servers. +|[[action-config-verification-mode]] +`xpack.actions[n]` +`.tls.verificationMode` {ess-icon} +| Controls the verification for the server certificate that {hosted-ems} receives when making an outbound SSL/TLS connection for actions. Valid values are `full`, `certificate`, and `none`. + Use `full` to perform hostname verification, `certificate` to skip hostname verification, and `none` to skip verification. Default: `full`. <>. + + + + As an alternative to setting `xpack.actions.tls.verificationMode`, you can use the setting + `xpack.actions.customHostSettings` to set TLS options for specific servers. + + + | `xpack.actions.maxResponseContentLength` {ess-icon} - | Specifies the max number of bytes of the http response for requests to external resources. Defaults to 1000000 (1MB). + | Specifies the max number of bytes of the http response for requests to external resources. Default: 1000000 (1MB). | `xpack.actions.responseTimeout` {ess-icon} | Specifies the time allowed for requests to external resources. Requests that take longer are aborted. The time is formatted as: + + `[ms,s,m,h,d,w,M,Y]` + + - For example, `20m`, `24h`, `7d`, `1w`. Defaults to `60s`. + For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. |=== diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 6daf252c524ddd..e4faa81c174e93 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -99,6 +99,7 @@ include::{kib-repo-dir}/api/spaces-management.asciidoc[] include::{kib-repo-dir}/api/role-management.asciidoc[] include::{kib-repo-dir}/api/session-management.asciidoc[] include::{kib-repo-dir}/api/saved-objects.asciidoc[] +include::{kib-repo-dir}/api/index-patterns.asciidoc[] include::{kib-repo-dir}/api/alerting.asciidoc[] include::{kib-repo-dir}/api/actions-and-connectors.asciidoc[] include::{kib-repo-dir}/api/dashboard-api.asciidoc[] diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 397ab1717183b8..c5fabb15dc4de2 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -24,10 +24,6 @@ and enrichments on your data. | {logstash-ref}/logstash-centralized-pipeline-management.html[Logstash Pipelines] | Create, edit, and delete your Logstash pipeline configurations. -| <> -| Manage your Beats configurations in a central location and -quickly deploy configuration changes to all Beats running across your enterprise. - |=== @@ -182,8 +178,6 @@ next major version of {es}, and then reindex, if needed. include::{kib-repo-dir}/management/advanced-options.asciidoc[] -include::{kib-repo-dir}/management/managing-beats.asciidoc[] - include::{kib-repo-dir}/management/action-types.asciidoc[] include::{kib-repo-dir}/management/managing-licenses.asciidoc[] diff --git a/package.json b/package.json index 73f3e5585faf74..627e8abd9d259c 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "dependencies": { "@elastic/apm-rum": "^5.6.1", "@elastic/apm-rum-react": "^1.2.5", - "@elastic/charts": "29.1.0", + "@elastic/charts": "29.2.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.13.0", @@ -136,7 +136,7 @@ "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", - "@kbn/monaco": "link:packages/kbn-monaco", + "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco/npm_module", "@kbn/securitysolution-list-constants": "link:bazel-bin/packages/kbn-securitysolution-list-constants/npm_module", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", @@ -158,9 +158,10 @@ "@loaders.gl/core": "^2.3.1", "@loaders.gl/json": "^2.3.1", "@mapbox/geojson-rewind": "^0.5.0", - "@mapbox/mapbox-gl-draw": "^1.2.0", - "@mapbox/mapbox-gl-rtl-text": "^0.2.3", + "@mapbox/mapbox-gl-draw": "1.3.0", + "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mapbox/vector-tile": "1.3.1", + "@reduxjs/toolkit": "^1.5.1", "@scant/router": "^0.1.1", "@slack/webhook": "^5.0.4", "@turf/along": "6.0.1", @@ -173,6 +174,7 @@ "@turf/distance": "6.0.1", "@turf/helpers": "6.0.1", "@turf/length": "^6.0.2", + "@types/redux-logger": "^3.0.8", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.4.0", @@ -285,7 +287,7 @@ "lru-cache": "^4.1.5", "lz-string": "^1.4.4", "mapbox-gl": "1.13.1", - "mapbox-gl-draw-rectangle-mode": "^1.0.4", + "mapbox-gl-draw-rectangle-mode": "1.0.4", "markdown-it": "^10.0.0", "md5": "^2.1.0", "memoize-one": "^5.0.0", @@ -365,6 +367,7 @@ "redux": "^4.0.5", "redux-actions": "^2.6.5", "redux-devtools-extension": "^2.13.8", + "redux-logger": "^3.0.6", "redux-observable": "^1.2.0", "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", @@ -562,7 +565,7 @@ "@types/loader-utils": "^1.1.3", "@types/lodash": "^4.14.159", "@types/lru-cache": "^5.1.0", - "@types/mapbox-gl": "^1.9.1", + "@types/mapbox-gl": "1.13.1", "@types/markdown-it": "^0.0.7", "@types/md5": "^2.2.0", "@types/memoize-one": "^4.1.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 43528e0ae41629..c885666f7a916e 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -25,6 +25,7 @@ filegroup( "//packages/kbn-legacy-logging:build", "//packages/kbn-logging:build", "//packages/kbn-mapbox-gl:build", + "//packages/kbn-monaco:build", "//packages/kbn-plugin-generator:build", "//packages/kbn-securitysolution-list-constants:build", "//packages/kbn-securitysolution-io-ts-types:build", diff --git a/packages/kbn-analytics/BUILD.bazel b/packages/kbn-analytics/BUILD.bazel index 1749a3dcdc8160..9eeaf4dc5842e9 100644 --- a/packages/kbn-analytics/BUILD.bazel +++ b/packages/kbn-analytics/BUILD.bazel @@ -56,10 +56,10 @@ ts_project( srcs = SRCS, deps = DEPS, declaration = True, - declaration_dir = "types", + declaration_dir = "target_types", declaration_map = True, incremental = True, - out_dir = "node", + out_dir = "target_node", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -72,38 +72,16 @@ ts_project( deps = DEPS, declaration = False, incremental = True, - out_dir = "web", + out_dir = "target_web", source_map = True, root_dir = "src", tsconfig = ":tsconfig_browser", ) -filegroup( - name = "tsc_types", - srcs = [":tsc"], - output_group = "types", -) - -filegroup( - name = "target_files", - srcs = [ - ":tsc", - ":tsc_browser", - ":tsc_types", - ], -) - -pkg_npm( - name = "target", - deps = [ - ":target_files", - ], -) - js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":target"], + deps = DEPS + [":tsc", ":tsc_browser"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-analytics/package.json b/packages/kbn-analytics/package.json index 726b62e1c61b80..177c0eb8157605 100644 --- a/packages/kbn-analytics/package.json +++ b/packages/kbn-analytics/package.json @@ -3,9 +3,9 @@ "private": true, "version": "1.0.0", "description": "Kibana Analytics tool", - "main": "target/node/index.js", - "types": "target/types/index.d.ts", - "browser": "target/web/index.js", + "main": "target_node/index.js", + "types": "target_types/index.d.ts", + "browser": "target_web/index.js", "author": "Ahmad Bamieh ", "license": "SSPL-1.0 OR Elastic License 2.0" } \ No newline at end of file diff --git a/packages/kbn-analytics/tsconfig.browser.json b/packages/kbn-analytics/tsconfig.browser.json index 12f70b77008e75..8a65c551d3bc41 100644 --- a/packages/kbn-analytics/tsconfig.browser.json +++ b/packages/kbn-analytics/tsconfig.browser.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.browser.json", "compilerOptions": { "incremental": true, - "outDir": "./target/web", + "outDir": "./target_web", "stripInternal": true, "declaration": false, "isolatedModules": true, diff --git a/packages/kbn-analytics/tsconfig.json b/packages/kbn-analytics/tsconfig.json index 165a8b695db572..2eaa88cd3ce5f6 100644 --- a/packages/kbn-analytics/tsconfig.json +++ b/packages/kbn-analytics/tsconfig.json @@ -2,8 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "incremental": true, - "declarationDir": "./target/types", - "outDir": "./target/node", + "declarationDir": "./target_types", + "outDir": "./target_node", "stripInternal": true, "declaration": true, "declarationMap": true, diff --git a/packages/kbn-babel-preset/BUILD.bazel b/packages/kbn-babel-preset/BUILD.bazel index 06b788010bdf51..f5ebc153b9e1a0 100644 --- a/packages/kbn-babel-preset/BUILD.bazel +++ b/packages/kbn-babel-preset/BUILD.bazel @@ -34,6 +34,7 @@ DEPS = [ "@npm//@babel/preset-typescript", "@npm//babel-plugin-add-module-exports", "@npm//babel-plugin-styled-components", + "@npm//babel-plugin-transform-react-remove-prop-types", ] js_library( diff --git a/packages/kbn-config/src/config_service.mock.ts b/packages/kbn-config/src/config_service.mock.ts index 83fbf20b5c0b3d..68adbba7c0ed73 100644 --- a/packages/kbn-config/src/config_service.mock.ts +++ b/packages/kbn-config/src/config_service.mock.ts @@ -26,11 +26,13 @@ const createConfigServiceMock = ({ addDeprecationProvider: jest.fn(), validate: jest.fn(), getHandledDeprecatedConfigs: jest.fn(), + getDeprecatedConfigPath$: jest.fn(), }; mocked.atPath.mockReturnValue(new BehaviorSubject(atPath)); mocked.atPathSync.mockReturnValue(atPath); mocked.getConfig$.mockReturnValue(new BehaviorSubject(new ObjectToConfigAdapter(getConfig$))); + mocked.getDeprecatedConfigPath$.mockReturnValue(new BehaviorSubject({ set: [], unset: [] })); mocked.getUsedPaths.mockResolvedValue([]); mocked.getUnusedPaths.mockResolvedValue([]); mocked.isEnabledAtPath.mockResolvedValue(true); diff --git a/packages/kbn-config/src/config_service.test.mocks.ts b/packages/kbn-config/src/config_service.test.mocks.ts index d8da2852b92515..39aa551ae85f95 100644 --- a/packages/kbn-config/src/config_service.test.mocks.ts +++ b/packages/kbn-config/src/config_service.test.mocks.ts @@ -11,10 +11,17 @@ import type { applyDeprecations } from './deprecation/apply_deprecations'; jest.mock('../../../package.json', () => mockPackage); +const changedPaths = { + set: ['foo'], + unset: ['bar.baz'], +}; + +export { changedPaths as mockedChangedPaths }; + export const mockApplyDeprecations = jest.fn< - Record, + ReturnType, Parameters ->((config, deprecations, createAddDeprecation) => config); +>((config, deprecations, createAddDeprecation) => ({ config, changedPaths })); jest.mock('./deprecation/apply_deprecations', () => ({ applyDeprecations: mockApplyDeprecations, diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index 64404341bc64d0..c2d4f15b6d9153 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -9,7 +9,7 @@ import { BehaviorSubject, Observable } from 'rxjs'; import { first, take } from 'rxjs/operators'; -import { mockApplyDeprecations } from './config_service.test.mocks'; +import { mockApplyDeprecations, mockedChangedPaths } from './config_service.test.mocks'; import { rawConfigServiceMock } from './raw/raw_config_service.mock'; import { schema } from '@kbn/config-schema'; @@ -420,7 +420,7 @@ test('logs deprecation warning during validation', async () => { const addDeprecation = createAddDeprecation!(''); addDeprecation({ message: 'some deprecation message' }); addDeprecation({ message: 'another deprecation message' }); - return config; + return { config, changedPaths: mockedChangedPaths }; }); loggerMock.clear(logger); @@ -446,12 +446,12 @@ test('does not log warnings for silent deprecations during validation', async () const addDeprecation = createAddDeprecation!(''); addDeprecation({ message: 'some deprecation message', silent: true }); addDeprecation({ message: 'another deprecation message' }); - return config; + return { config, changedPaths: mockedChangedPaths }; }) .mockImplementationOnce((config, deprecations, createAddDeprecation) => { const addDeprecation = createAddDeprecation!(''); addDeprecation({ message: 'I am silent', silent: true }); - return config; + return { config, changedPaths: mockedChangedPaths }; }); loggerMock.clear(logger); @@ -521,7 +521,7 @@ describe('getHandledDeprecatedConfigs', () => { const addDeprecation = createAddDeprecation!(deprecation.path); addDeprecation({ message: `some deprecation message`, documentationUrl: 'some-url' }); }); - return config; + return { config, changedPaths: mockedChangedPaths }; }); await configService.validate(); @@ -541,3 +541,18 @@ describe('getHandledDeprecatedConfigs', () => { `); }); }); + +describe('getDeprecatedConfigPath$', () => { + it('returns all config paths changes during deprecation', async () => { + const rawConfig$ = new BehaviorSubject>({ key: 'value' }); + const rawConfigProvider = rawConfigServiceMock.create({ rawConfig$ }); + + const configService = new ConfigService(rawConfigProvider, defaultEnv, logger); + await configService.setSchema('key', schema.string()); + await configService.validate(); + + const deprecatedConfigPath$ = configService.getDeprecatedConfigPath$(); + const deprecatedConfigPath = await deprecatedConfigPath$.pipe(first()).toPromise(); + expect(deprecatedConfigPath).toEqual(mockedChangedPaths); + }); +}); diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index 91927b4c7b5c96..a80680bd46dfca 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -22,6 +22,7 @@ import { ConfigDeprecationProvider, configDeprecationFactory, DeprecatedConfigDetails, + ChangedDeprecatedPaths, } from './deprecation'; import { LegacyObjectToConfigAdapter } from './legacy'; @@ -36,6 +37,10 @@ export class ConfigService { private validated = false; private readonly config$: Observable; private lastConfig?: Config; + private readonly deprecatedConfigPaths = new BehaviorSubject({ + set: [], + unset: [], + }); /** * Whenever a config if read at a path, we mark that path as 'handled'. We can @@ -57,7 +62,8 @@ export class ConfigService { this.config$ = combineLatest([this.rawConfigProvider.getConfig$(), this.deprecations]).pipe( map(([rawConfig, deprecations]) => { const migrated = applyDeprecations(rawConfig, deprecations); - return new LegacyObjectToConfigAdapter(migrated); + this.deprecatedConfigPaths.next(migrated.changedPaths); + return new LegacyObjectToConfigAdapter(migrated.config); }), tap((config) => { this.lastConfig = config; @@ -191,6 +197,10 @@ export class ConfigService { return config.getFlattenedPaths().filter((path) => isPathHandled(path, handledPaths)); } + public getDeprecatedConfigPath$() { + return this.deprecatedConfigPaths.asObservable(); + } + private async logDeprecation() { const rawConfig = await this.rawConfigProvider.getConfig$().pipe(take(1)).toPromise(); const deprecations = await this.deprecations.pipe(take(1)).toPromise(); diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts index 47746967bbe5f1..8ad1491c19c9b1 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.test.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.test.ts @@ -82,7 +82,7 @@ describe('applyDeprecations', () => { it('returns the migrated config', () => { const initialConfig = { foo: 'bar', deprecated: 'deprecated', renamed: 'renamed' }; - const migrated = applyDeprecations(initialConfig, [ + const { config: migrated } = applyDeprecations(initialConfig, [ wrapHandler(deprecations.unused('deprecated')), wrapHandler(deprecations.rename('renamed', 'newname')), ]); @@ -93,7 +93,7 @@ describe('applyDeprecations', () => { it('does not alter the initial config', () => { const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; - const migrated = applyDeprecations(initialConfig, [ + const { config: migrated } = applyDeprecations(initialConfig, [ wrapHandler(deprecations.unused('deprecated')), ]); @@ -110,7 +110,7 @@ describe('applyDeprecations', () => { return { unset: [{ path: 'unknown' }] }; }); - const migrated = applyDeprecations( + const { config: migrated } = applyDeprecations( initialConfig, [wrapHandler(handler, 'pathA')], createAddDeprecation @@ -128,7 +128,7 @@ describe('applyDeprecations', () => { return { rewrite: [{ path: 'foo' }] }; }); - const migrated = applyDeprecations( + const { config: migrated } = applyDeprecations( initialConfig, [wrapHandler(handler, 'pathA')], createAddDeprecation @@ -136,4 +136,25 @@ describe('applyDeprecations', () => { expect(migrated).toEqual(initialConfig); }); + + it('returns a list of changes config paths', () => { + const addDeprecation = jest.fn(); + const createAddDeprecation = jest.fn().mockReturnValue(addDeprecation); + const initialConfig = { foo: 'bar', deprecated: 'deprecated' }; + + const handler = jest.fn().mockImplementation((config) => { + return { set: [{ path: 'foo', value: 'bar' }], unset: [{ path: 'baz' }] }; + }); + + const { changedPaths } = applyDeprecations( + initialConfig, + [wrapHandler(handler, 'pathA')], + createAddDeprecation + ); + + expect(changedPaths).toEqual({ + set: ['foo'], + unset: ['baz'], + }); + }); }); diff --git a/packages/kbn-config/src/deprecation/apply_deprecations.ts b/packages/kbn-config/src/deprecation/apply_deprecations.ts index 092a5ced28371d..d38ae988358313 100644 --- a/packages/kbn-config/src/deprecation/apply_deprecations.ts +++ b/packages/kbn-config/src/deprecation/apply_deprecations.ts @@ -8,7 +8,11 @@ import { cloneDeep, unset } from 'lodash'; import { set } from '@elastic/safer-lodash-set'; -import { ConfigDeprecationWithContext, AddConfigDeprecation } from './types'; +import type { + AddConfigDeprecation, + ChangedDeprecatedPaths, + ConfigDeprecationWithContext, +} from './types'; const noopAddDeprecationFactory: () => AddConfigDeprecation = () => () => undefined; /** @@ -22,22 +26,31 @@ export const applyDeprecations = ( config: Record, deprecations: ConfigDeprecationWithContext[], createAddDeprecation: (pluginId: string) => AddConfigDeprecation = noopAddDeprecationFactory -) => { +): { config: Record; changedPaths: ChangedDeprecatedPaths } => { const result = cloneDeep(config); + const changedPaths: ChangedDeprecatedPaths = { + set: [], + unset: [], + }; deprecations.forEach(({ deprecation, path }) => { const commands = deprecation(result, path, createAddDeprecation(path)); if (commands) { if (commands.set) { + changedPaths.set.push(...commands.set.map((c) => c.path)); commands.set.forEach(function ({ path: commandPath, value }) { set(result, commandPath, value); }); } if (commands.unset) { + changedPaths.unset.push(...commands.unset.map((c) => c.path)); commands.unset.forEach(function ({ path: commandPath }) { unset(result, commandPath); }); } } }); - return result; + return { + config: result, + changedPaths, + }; }; diff --git a/packages/kbn-config/src/deprecation/index.ts b/packages/kbn-config/src/deprecation/index.ts index 48576e6d830bef..ce10bafd9c575b 100644 --- a/packages/kbn-config/src/deprecation/index.ts +++ b/packages/kbn-config/src/deprecation/index.ts @@ -14,6 +14,7 @@ export type { AddConfigDeprecation, ConfigDeprecationProvider, DeprecatedConfigDetails, + ChangedDeprecatedPaths, } from './types'; export { configDeprecationFactory } from './deprecation_factory'; export { applyDeprecations } from './apply_deprecations'; diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 6944f45c1e1d22..0522365ad76c11 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -55,6 +55,16 @@ export type ConfigDeprecation = ( addDeprecation: AddConfigDeprecation ) => void | ConfigDeprecationCommand; +/** + * List of config paths changed during deprecation. + * + * @public + */ +export interface ChangedDeprecatedPaths { + set: string[]; + unset: string[]; +} + /** * Outcome of deprecation operation. Allows mutating config values in a declarative way. * diff --git a/packages/kbn-config/src/index.ts b/packages/kbn-config/src/index.ts index cf875d3daa4a21..294caba4e7048b 100644 --- a/packages/kbn-config/src/index.ts +++ b/packages/kbn-config/src/index.ts @@ -13,6 +13,7 @@ export type { ConfigDeprecationWithContext, ConfigDeprecation, ConfigDeprecationCommand, + ChangedDeprecatedPaths, } from './deprecation'; export { applyDeprecations, configDeprecationFactory } from './deprecation'; diff --git a/packages/kbn-mapbox-gl/src/index.ts b/packages/kbn-mapbox-gl/src/index.ts index 117b874a28ffbd..404684af780310 100644 --- a/packages/kbn-mapbox-gl/src/index.ts +++ b/packages/kbn-mapbox-gl/src/index.ts @@ -7,6 +7,24 @@ */ import './typings'; +import type { + Map, + GeoJSONSource, + VectorSource, + Layer, + AnyLayer, + FeatureIdentifier, + Style, + MapboxOptions, + MapMouseEvent, + MapSourceDataEvent, + LngLat, + LngLatBounds, + PointLike, + MapboxGeoJSONFeature, + Point, + CustomLayerInterface, +} from 'mapbox-gl'; import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; // @ts-expect-error import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; @@ -17,4 +35,22 @@ import 'mapbox-gl/dist/mapbox-gl.css'; mapboxgl.workerUrl = mbWorkerUrl; mapboxgl.setRTLTextPlugin(mbRtlPlugin); -export { mapboxgl }; +export { + mapboxgl, + Map, + GeoJSONSource, + VectorSource, + Layer, + AnyLayer, + FeatureIdentifier, + Style, + MapboxOptions, + MapMouseEvent, + MapSourceDataEvent, + LngLat, + LngLatBounds, + PointLike, + MapboxGeoJSONFeature, + Point, + CustomLayerInterface, +}; diff --git a/packages/kbn-monaco/BUILD.bazel b/packages/kbn-monaco/BUILD.bazel new file mode 100644 index 00000000000000..3a25568dfd811d --- /dev/null +++ b/packages/kbn-monaco/BUILD.bazel @@ -0,0 +1,108 @@ +load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("@npm//webpack-cli:index.bzl", webpack = "webpack_cli") + +PKG_BASE_NAME = "kbn-monaco" +PKG_REQUIRE_NAME = "@kbn/monaco" + +SOURCE_FILES = glob( + [ + "src/**/*", + ], + exclude = [ + "**/*.test.*", + "**/README.md", + ], +) + +SRCS = SOURCE_FILES + +filegroup( + name = "srcs", + srcs = SRCS, +) + +NPM_MODULE_EXTRA_FILES = [ + "package.json", + "README.md" +] + +SRC_DEPS = [ + "//packages/kbn-babel-preset", + "//packages/kbn-i18n", + "@npm//antlr4ts", + "@npm//babel-loader", + "@npm//monaco-editor", + "@npm//raw-loader", + "@npm//regenerator-runtime", +] + +TYPES_DEPS = [ + "@npm//@types/jest", + "@npm//@types/node", +] + +DEPS = SRC_DEPS + TYPES_DEPS + +webpack( + name = "target_web", + data = DEPS + [ + ":src", + ":webpack.config.js", + ], + output_dir = True, + args = [ + "--config", + "$(location webpack.config.js)", + "--output-path", + "$(@D)", + "--env.prod", + "--display=minimal" + ], +) + +ts_config( + name = "tsconfig", + src = "tsconfig.json", + deps = [ + "//:tsconfig.base.json", + ], +) + +ts_project( + name = "tsc", + args = ['--pretty'], + srcs = SRCS, + deps = DEPS, + declaration = True, + declaration_dir = "target_types", + declaration_map = True, + incremental = True, + out_dir = "target_node", + source_map = True, + root_dir = ".", + tsconfig = ":tsconfig", +) + +js_library( + name = PKG_BASE_NAME, + srcs = NPM_MODULE_EXTRA_FILES, + deps = DEPS + [":target_web", ":tsc"], + package_name = PKG_REQUIRE_NAME, + visibility = ["//visibility:public"], +) + +pkg_npm( + name = "npm_module", + deps = [ + ":%s" % PKG_BASE_NAME, + ] +) + +filegroup( + name = "build", + srcs = [ + ":npm_module", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-monaco/package.json b/packages/kbn-monaco/package.json index e818351e7e4700..4f372d8ae62dd4 100644 --- a/packages/kbn-monaco/package.json +++ b/packages/kbn-monaco/package.json @@ -2,12 +2,10 @@ "name": "@kbn/monaco", "version": "1.0.0", "private": true, - "main": "./target/index.js", - "types": "./target/index.d.ts", + "main": "target_node/src/index.js", + "types": "target_types/src/index.d.ts", "license": "SSPL-1.0 OR Elastic License 2.0", "scripts": { - "build": "node ./scripts/build.js", - "kbn:bootstrap": "yarn build --dev", "build:antlr4ts": "../../node_modules/antlr4ts-cli/antlr4ts ./src/painless/antlr/painless_lexer.g4 ./src/painless/antlr/painless_parser.g4 && node ./scripts/fix_generated_antlr.js" } } diff --git a/packages/kbn-monaco/scripts/build.js b/packages/kbn-monaco/scripts/build.js deleted file mode 100644 index 96159b4ad0dca8..00000000000000 --- a/packages/kbn-monaco/scripts/build.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 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. - */ - -const path = require('path'); -const del = require('del'); -const supportsColor = require('supports-color'); -const { run } = require('@kbn/dev-utils'); - -const TARGET_BUILD_DIR = path.resolve(__dirname, '../target'); -const ROOT_DIR = path.resolve(__dirname, '../'); -const WEBPACK_CONFIG_PATH = path.resolve(ROOT_DIR, 'webpack.config.js'); - -run( - async ({ procRunner, log, flags }) => { - log.info('Deleting old output'); - - await del(TARGET_BUILD_DIR); - - const cwd = ROOT_DIR; - const env = { ...process.env }; - if (supportsColor.stdout) { - env.FORCE_COLOR = 'true'; - } - - await procRunner.run('worker', { - cmd: 'webpack', - args: ['--config', WEBPACK_CONFIG_PATH, flags.dev ? '--env.dev' : '--env.prod'], - wait: true, - env, - cwd, - }); - - await procRunner.run('tsc ', { - cmd: 'tsc', - args: [], - wait: true, - env, - cwd, - }); - - log.success('Complete'); - }, - { - flags: { - boolean: ['dev'], - }, - } -); diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index a07d979e2022b6..4047ddedeca42d 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -11,11 +11,11 @@ import { PainlessLang } from './painless'; import { EsqlLang } from './esql'; import { monaco } from './monaco_imports'; // @ts-ignore -import xJsonWorkerSrc from '!!raw-loader!../target/public/xjson.editor.worker.js'; +import xJsonWorkerSrc from '!!raw-loader!../../target_web/xjson.editor.worker.js'; // @ts-ignore -import defaultWorkerSrc from '!!raw-loader!../target/public/default.editor.worker.js'; +import defaultWorkerSrc from '!!raw-loader!../../target_web/default.editor.worker.js'; // @ts-ignore -import painlessWorkerSrc from '!!raw-loader!../target/public/painless.editor.worker.js'; +import painlessWorkerSrc from '!!raw-loader!../../target_web/painless.editor.worker.js'; /** * Register languages and lexer rules diff --git a/packages/kbn-monaco/tsconfig.json b/packages/kbn-monaco/tsconfig.json index e6ec96b12c6cf1..3c17118337899c 100644 --- a/packages/kbn-monaco/tsconfig.json +++ b/packages/kbn-monaco/tsconfig.json @@ -1,10 +1,12 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "incremental": false, - "outDir": "./target", + "incremental": true, + "declarationDir": "./target_types", + "outDir": "./target_node", "declaration": true, "declarationMap": true, + "rootDir": ".", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-monaco/src", "types": [ @@ -13,6 +15,6 @@ ] }, "include": [ - "src/**/*" + "src/**/*", ] } diff --git a/packages/kbn-monaco/webpack.config.js b/packages/kbn-monaco/webpack.config.js index 5ee00565e6494d..d035134565463e 100644 --- a/packages/kbn-monaco/webpack.config.js +++ b/packages/kbn-monaco/webpack.config.js @@ -18,7 +18,7 @@ const createLangWorkerConfig = (lang) => { mode: 'production', entry, output: { - path: path.resolve(__dirname, 'target/public'), + path: path.resolve(__dirname, 'target_web'), filename: `${lang}.editor.worker.js`, }, resolve: { diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index a585a0fc7542f5..c28fd835919602 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -3,7 +3,6 @@ pageLoadAssetSize: alerting: 106936 apm: 64385 apmOss: 18996 - beatsManagement: 188135 bfetch: 41874 canvas: 1066647 charts: 195358 @@ -77,7 +76,7 @@ pageLoadAssetSize: tileMap: 65337 timelion: 29920 transform: 41007 - triggersActionsUi: 186732 + triggersActionsUi: 100000 uiActions: 97717 uiActionsEnhanced: 313011 upgradeAssistant: 81241 diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index c0afb92b859cd2..29c0457c316f06 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -30541,29 +30541,14 @@ module.exports = function nodeModulesPaths(start, opts, request) { var isWindows = process.platform === 'win32'; -// Regex to split a windows path into three parts: [*, device, slash, -// tail] windows-only -var splitDeviceRe = - /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?([\\\/])?([\s\S]*?)$/; - -// Regex to split the tail part of the above into [*, dir, basename, ext] -var splitTailRe = - /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/; +// Regex to split a windows path into into [dir, root, basename, name, ext] +var splitWindowsRe = + /^(((?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/]+[^\\\/]+)?[\\\/]?)(?:[^\\\/]*[\\\/])*)((\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))[\\\/]*$/; var win32 = {}; -// Function to split a filename into [root, dir, basename, ext] function win32SplitPath(filename) { - // Separate device+slash from tail - var result = splitDeviceRe.exec(filename), - device = (result[1] || '') + (result[2] || ''), - tail = result[3] || ''; - // Split the tail into dir, basename and extension - var result2 = splitTailRe.exec(tail), - dir = result2[1], - basename = result2[2], - ext = result2[3]; - return [device, dir, basename, ext]; + return splitWindowsRe.exec(filename).slice(1); } win32.parse = function(pathString) { @@ -30573,24 +30558,24 @@ win32.parse = function(pathString) { ); } var allParts = win32SplitPath(pathString); - if (!allParts || allParts.length !== 4) { + if (!allParts || allParts.length !== 5) { throw new TypeError("Invalid path '" + pathString + "'"); } return { - root: allParts[0], - dir: allParts[0] + allParts[1].slice(0, -1), + root: allParts[1], + dir: allParts[0] === allParts[1] ? allParts[0] : allParts[0].slice(0, -1), base: allParts[2], - ext: allParts[3], - name: allParts[2].slice(0, allParts[2].length - allParts[3].length) + ext: allParts[4], + name: allParts[3] }; }; -// Split a filename into [root, dir, basename, ext], unix version +// Split a filename into [dir, root, basename, name, ext], unix version // 'root' is just a slash, or nothing. var splitPathRe = - /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + /^((\/?)(?:[^\/]*\/)*)((\.{1,2}|[^\/]+?|)(\.[^.\/]*|))[\/]*$/; var posix = {}; @@ -30606,19 +30591,16 @@ posix.parse = function(pathString) { ); } var allParts = posixSplitPath(pathString); - if (!allParts || allParts.length !== 4) { + if (!allParts || allParts.length !== 5) { throw new TypeError("Invalid path '" + pathString + "'"); } - allParts[1] = allParts[1] || ''; - allParts[2] = allParts[2] || ''; - allParts[3] = allParts[3] || ''; - + return { - root: allParts[0], - dir: allParts[0] + allParts[1].slice(0, -1), + root: allParts[1], + dir: allParts[0].slice(0, -1), base: allParts[2], - ext: allParts[3], - name: allParts[2].slice(0, allParts[2].length - allParts[3].length) + ext: allParts[4], + name: allParts[3], }; }; diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts index 3ce38733dfd249..5895ef193fbfe2 100644 --- a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -26,7 +26,7 @@ const template: string = `module.exports = { }; `; -const roots: string[] = ['x-pack/plugins', 'packages', 'src/legacy', 'src/plugins', 'test', 'src']; +const roots: string[] = ['x-pack/plugins', 'packages', 'src/plugins', 'test', 'src']; export async function runCheckJestConfigsCli() { run( diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index c284be4487a5f0..162606585c43e5 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -7,8 +7,5 @@ "build": "node scripts/build", "kbn:bootstrap": "node scripts/build --dev", "kbn:watch": "node scripts/build --dev --watch" - }, - "dependencies": { - "@kbn/monaco": "link:../kbn-monaco" } } \ No newline at end of file diff --git a/renovate.json5 b/renovate.json5 index ea41175e1aaab5..f533eac4796508 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -12,6 +12,7 @@ baseBranches: [ 'master', '7.x', + '7.13', ], prConcurrentLimit: 0, prHourlyLimit: 0, @@ -44,17 +45,25 @@ { groupName: '@elastic/elasticsearch', packageNames: ['@elastic/elasticsearch'], - reviewers: ['team:kibana-operations'], + reviewers: ['team:kibana-operations', 'team:kibana-core'], matchBaseBranches: ['master'], - labels: ['release_note:skip', 'v8.0.0', 'Team:Operations', 'backport:skip'], + labels: ['release_note:skip', 'v8.0.0', 'Team:Operations', 'Team:Core', 'backport:skip'], enabled: true, }, { groupName: '@elastic/elasticsearch', packageNames: ['@elastic/elasticsearch'], - reviewers: ['team:kibana-operations'], + reviewers: ['team:kibana-operations', 'team:kibana-core'], matchBaseBranches: ['7.x'], - labels: ['release_note:skip', 'v7.14.0', 'Team:Operations', 'backport:skip'], + labels: ['release_note:skip', 'v7.14.0', 'Team:Operations', 'Team:Core', 'backport:skip'], + enabled: true, + }, + { + groupName: '@elastic/elasticsearch', + packageNames: ['@elastic/elasticsearch'], + reviewers: ['team:kibana-operations'], + matchBaseBranches: ['7.13'], + labels: ['release_note:skip', 'v7.13.0', 'Team:Operations', 'backport:skip'], enabled: true, }, { diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index b2eec43cc5ad77..36d613ec82f9e0 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -131,6 +131,7 @@ export class DocLinksService { introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`, fieldFormattersNumber: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/numeral.html`, fieldFormattersString: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/field-formatters-string.html`, + runtimeFields: `${KIBANA_DOCS}managing-index-patterns.html#runtime-fields`, }, addData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/connect-to-elasticsearch.html`, kibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index.html`, @@ -512,6 +513,7 @@ export interface DocLinksStart { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; + readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 9f0c5135e702fa..667863d29623ed 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -584,6 +584,7 @@ export interface DocLinksStart { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; + readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; diff --git a/src/core/server/config/test_utils.ts b/src/core/server/config/test_utils.ts index 8e20e87e6f7d8e..ab06ff50012b7b 100644 --- a/src/core/server/config/test_utils.ts +++ b/src/core/server/config/test_utils.ts @@ -16,7 +16,7 @@ function collectDeprecations( ) { const deprecations = provider(configDeprecationFactory); const deprecationMessages: string[] = []; - const migrated = applyDeprecations( + const { config: migrated } = applyDeprecations( settings, deprecations.map((deprecation) => ({ deprecation, diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts index e09f595747c30b..5fa67fecb2a8ae 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts @@ -116,6 +116,10 @@ const createStartContractMock = () => { maxImportExportSize: 10000, maxImportPayloadBytes: 26214400, }, + deprecatedKeys: { + set: ['path.to.a.prop'], + unset: [], + }, }, environment: { memory: { diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts index dc74b65c8dcfc2..95dd392016c179 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.test.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts @@ -91,7 +91,8 @@ describe('CoreUsageDataService', () => { const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - service.setup({ http, metrics, savedObjectsStartPromise }); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + service.setup({ http, metrics, savedObjectsStartPromise, changedDeprecatedConfigPath$ }); const savedObjects = await savedObjectsStartPromise; expect(savedObjects.createInternalRepository).toHaveBeenCalledTimes(1); @@ -105,7 +106,13 @@ describe('CoreUsageDataService', () => { const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - const coreUsageData = service.setup({ http, metrics, savedObjectsStartPromise }); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + const coreUsageData = service.setup({ + http, + metrics, + savedObjectsStartPromise, + changedDeprecatedConfigPath$, + }); const typeRegistry = typeRegistryMock.create(); coreUsageData.registerType(typeRegistry); @@ -126,7 +133,13 @@ describe('CoreUsageDataService', () => { const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - const coreUsageData = service.setup({ http, metrics, savedObjectsStartPromise }); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + const coreUsageData = service.setup({ + http, + metrics, + savedObjectsStartPromise, + changedDeprecatedConfigPath$, + }); const usageStatsClient = coreUsageData.getClient(); expect(usageStatsClient).toBeInstanceOf(CoreUsageStatsClient); @@ -142,7 +155,11 @@ describe('CoreUsageDataService', () => { const savedObjectsStartPromise = Promise.resolve( savedObjectsServiceMock.createStartContract() ); - service.setup({ http, metrics, savedObjectsStartPromise }); + const changedDeprecatedConfigPath$ = new BehaviorSubject({ + set: ['new.path'], + unset: ['deprecated.path'], + }); + service.setup({ http, metrics, savedObjectsStartPromise, changedDeprecatedConfigPath$ }); const elasticsearch = elasticsearchServiceMock.createStart(); elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({ body: [ @@ -180,6 +197,14 @@ describe('CoreUsageDataService', () => { expect(getCoreUsageData()).resolves.toMatchInlineSnapshot(` Object { "config": Object { + "deprecatedKeys": Object { + "set": Array [ + "new.path", + ], + "unset": Array [ + "deprecated.path", + ], + }, "elasticsearch": Object { "apiVersion": "master", "customHeadersConfigured": false, @@ -381,12 +406,10 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginA.enabled": "[redacted]", - "pluginAB.enabled": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginA.enabled': '[redacted]', + 'pluginAB.enabled': '[redacted]', + }); }); it('returns an object of plugin config usage', async () => { @@ -418,23 +441,21 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "elasticsearch.password": "[redacted]", - "elasticsearch.username": "[redacted]", - "logging.json": false, - "pluginA.arrayOfNumbers": "[redacted]", - "pluginA.enabled": true, - "pluginA.objectConfig.debug": true, - "pluginA.objectConfig.username": "[redacted]", - "pluginAB.enabled": false, - "pluginB.arrayOfObjects": "[redacted]", - "plugins.paths": "[redacted]", - "server.basePath": "/zvt", - "server.port": 5603, - "server.rewriteBasePath": true, - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'elasticsearch.password': '[redacted]', + 'elasticsearch.username': '[redacted]', + 'logging.json': false, + 'pluginA.arrayOfNumbers': '[redacted]', + 'pluginA.enabled': true, + 'pluginA.objectConfig.debug': true, + 'pluginA.objectConfig.username': '[redacted]', + 'pluginAB.enabled': false, + 'pluginB.arrayOfObjects': '[redacted]', + 'plugins.paths': '[redacted]', + 'server.basePath': '/zvt', + 'server.port': 5603, + 'server.rewriteBasePath': true, + }); }); describe('config explicitly exposed to usage', () => { @@ -457,12 +478,10 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginA.objectConfig.debug": "[redacted]", - "server.basePath": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginA.objectConfig.debug': '[redacted]', + 'server.basePath': '[redacted]', + }); }); it('returns config value on safe complete match', async () => { @@ -478,11 +497,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "server.basePath": "/zvt", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'server.basePath': '/zvt', + }); }); it('returns [redacted] on unsafe parent match', async () => { @@ -501,12 +518,10 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginA.objectConfig.debug": "[redacted]", - "pluginA.objectConfig.username": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginA.objectConfig.debug': '[redacted]', + 'pluginA.objectConfig.username': '[redacted]', + }); }); it('returns config value on safe parent match', async () => { @@ -525,12 +540,10 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginA.objectConfig.debug": true, - "pluginA.objectConfig.username": "some_user", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginA.objectConfig.debug': true, + 'pluginA.objectConfig.username': 'some_user', + }); }); it('returns [redacted] on explicitly marked as safe array of objects', async () => { @@ -546,11 +559,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginB.arrayOfObjects": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginB.arrayOfObjects': '[redacted]', + }); }); it('returns values on explicitly marked as safe array of numbers', async () => { @@ -566,15 +577,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginA.arrayOfNumbers": Array [ - 1, - 2, - 3, - ], - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginA.arrayOfNumbers': [1, 2, 3], + }); }); it('returns values on explicitly marked as safe array of strings', async () => { @@ -590,15 +595,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "plugins.paths": Array [ - "pluginA", - "pluginAB", - "pluginB", - ], - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'plugins.paths': ['pluginA', 'pluginAB', 'pluginB'], + }); }); }); @@ -619,12 +618,10 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginA.objectConfig.debug": "[redacted]", - "pluginA.objectConfig.username": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginA.objectConfig.debug': '[redacted]', + 'pluginA.objectConfig.username': '[redacted]', + }); }); it('returns config value on safe parent match', async () => { @@ -640,13 +637,11 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "elasticsearch.password": "[redacted]", - "elasticsearch.username": "[redacted]", - "pluginA.objectConfig.username": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'elasticsearch.password': '[redacted]', + 'elasticsearch.username': '[redacted]', + 'pluginA.objectConfig.username': '[redacted]', + }); }); it('returns [redacted] on implicit array of objects', async () => { @@ -658,11 +653,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginB.arrayOfObjects": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginB.arrayOfObjects': '[redacted]', + }); }); it('returns values on implicit array of numbers', async () => { @@ -674,16 +667,11 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "pluginA.arrayOfNumbers": Array [ - 1, - 2, - 3, - ], - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'pluginA.arrayOfNumbers': [1, 2, 3], + }); }); + it('returns [redacted] on implicit array of strings', async () => { configService.getUsedPaths.mockResolvedValue(['plugins.paths']); @@ -693,11 +681,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "plugins.paths": "[redacted]", - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'plugins.paths': '[redacted]', + }); }); it('returns config value for numbers', async () => { @@ -709,11 +695,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "server.port": 5603, - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'server.port': 5603, + }); }); it('returns config value for booleans', async () => { @@ -728,12 +712,10 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "logging.json": false, - "pluginA.objectConfig.debug": true, - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'logging.json': false, + 'pluginA.objectConfig.debug': true, + }); }); it('ignores exposed to usage configs but not used', async () => { @@ -749,11 +731,9 @@ describe('CoreUsageDataService', () => { elasticsearch, }); - await expect(getConfigsUsageData()).resolves.toMatchInlineSnapshot(` - Object { - "logging.json": false, - } - `); + await expect(getConfigsUsageData()).resolves.toEqual({ + 'logging.json': false, + }); }); }); }); @@ -779,7 +759,8 @@ describe('CoreUsageDataService', () => { savedObjectsServiceMock.createStartContract() ); - service.setup({ http, metrics, savedObjectsStartPromise }); + const changedDeprecatedConfigPath$ = configServiceMock.create().getDeprecatedConfigPath$(); + service.setup({ http, metrics, savedObjectsStartPromise, changedDeprecatedConfigPath$ }); // Use the stopTimer$ to delay calling stop() until the third frame const stopTimer$ = cold('---a|'); diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts index 85abdca9ea5dc3..dc24f889cd8dd4 100644 --- a/src/core/server/core_usage_data/core_usage_data_service.ts +++ b/src/core/server/core_usage_data/core_usage_data_service.ts @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import { Subject } from 'rxjs'; +import { Subject, Observable } from 'rxjs'; import { takeUntil, first } from 'rxjs/operators'; import { get } from 'lodash'; -import { hasConfigPathIntersection } from '@kbn/config'; +import { hasConfigPathIntersection, ChangedDeprecatedPaths } from '@kbn/config'; import { CoreService } from 'src/core/types'; import { Logger, SavedObjectsServiceStart, SavedObjectTypeRegistry } from 'src/core/server'; @@ -39,6 +39,7 @@ export interface SetupDeps { http: InternalHttpServiceSetup; metrics: MetricsServiceSetup; savedObjectsStartPromise: Promise; + changedDeprecatedConfigPath$: Observable; } export interface StartDeps { @@ -89,6 +90,7 @@ export class CoreUsageDataService implements CoreService); } - setup({ http, metrics, savedObjectsStartPromise }: SetupDeps) { + setup({ http, metrics, savedObjectsStartPromise, changedDeprecatedConfigPath$ }: SetupDeps) { metrics .getOpsMetrics$() .pipe(takeUntil(this.stop$)) @@ -417,6 +421,10 @@ export class CoreUsageDataService implements CoreService (this.deprecatedConfigPaths = deprecatedConfigPaths)); + const internalRepositoryPromise = savedObjectsStartPromise.then((savedObjects) => savedObjects.createInternalRepository([CORE_USAGE_STATS_TYPE]) ); diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts index 1d5ef6d893f53e..affd3d5c66ab7a 100644 --- a/src/core/server/core_usage_data/types.ts +++ b/src/core/server/core_usage_data/types.ts @@ -254,6 +254,11 @@ export interface CoreConfigUsageData { // uiSettings: { // overridesCount: number; // }; + + deprecatedKeys: { + set: string[]; + unset: string[]; + }; } /** @internal */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index d9ad24a4a2c0c0..7f108dbeb0086f 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -399,6 +399,11 @@ export interface ContextSetup { // @internal export interface CoreConfigUsageData { + // (undocumented) + deprecatedKeys: { + set: string[]; + unset: string[]; + }; // (undocumented) elasticsearch: { sniffOnStart: boolean; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index fcfca3a5e0e2fa..4d99368f9bf708 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -153,6 +153,7 @@ export class Server { http: httpSetup, metrics: metricsSetup, savedObjectsStartPromise: this.savedObjectsStartPromise, + changedDeprecatedConfigPath$: this.configService.getDeprecatedConfigPath$(), }); const savedObjectsSetup = await this.savedObjects.setup({ @@ -265,6 +266,7 @@ export class Server { await this.http.start(); startTransaction?.end(); + return this.coreStart; } diff --git a/src/dev/build/tasks/clean_tasks.ts b/src/dev/build/tasks/clean_tasks.ts index 3051579d2e6f81..d4b4f98ed295b9 100644 --- a/src/dev/build/tasks/clean_tasks.ts +++ b/src/dev/build/tasks/clean_tasks.ts @@ -62,6 +62,7 @@ export const CleanExtraFilesFromModules: Task = { // tests '**/test', '**/tests', + '**/jest.config.js', '**/__tests__', '**/*.test.js', '**/*.snap', diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index 2f54bd1d818b56..a1838c571ea0be 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -175,6 +175,8 @@ kibana_vars=( xpack.actions.rejectUnauthorized xpack.actions.maxResponseContentLength xpack.actions.responseTimeout + xpack.actions.tls.verificationMode + xpack.actions.tls.proxyVerificationMode xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay @@ -231,6 +233,7 @@ kibana_vars=( xpack.maps.showMapVisualizationTypes xpack.ml.enabled xpack.observability.unsafe.alertingExperience.enabled + xpack.observability.unsafe.cases.enabled xpack.reporting.capture.browser.autoDownload xpack.reporting.capture.browser.chromium.disableSandbox xpack.reporting.capture.browser.chromium.inspect diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index c8eb16530507f9..e22d8ecdd4fd8e 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -18,6 +18,7 @@ function generator({ imageFlavor }: TemplateContext) { # Default Kibana configuration for docker target server.host: "0.0.0.0" + server.shutdownTimeout: "5s" elasticsearch.hosts: [ "http://elasticsearch:9200" ] ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} `); diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 3aed49b5015bb6..d523f78a9f5896 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -40,6 +40,7 @@ export const IGNORE_FILE_GLOBS = [ 'vars/*', '.ci/pipeline-library/**/*', 'packages/kbn-test/jest-preset.js', + 'test/package/Vagrantfile', // filename must match language code which requires capital letters '**/translations/*.json', @@ -94,7 +95,6 @@ export const IGNORE_DIRECTORY_GLOBS = [ ...KEBAB_CASE_DIRECTORY_GLOBS, 'src/babel-*', 'packages/*', - 'src/legacy/ui/public/flot-charts', 'test/functional/fixtures/es_archiver/visualize_source-filters', 'packages/kbn-pm/src/utils/__fixtures__/*', 'x-pack/dev-tools', diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts index 0c303b92bf1a1c..844e2c3b301fa1 100644 --- a/src/plugins/charts/public/static/utils/transform_click_event.ts +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -152,9 +152,9 @@ const rowFindPredicate = ( ) => (row: Datatable['rows'][number]): boolean => (geometry === null || (xAccessor !== null && - getAccessorValue(row, xAccessor) === geometry.x && + getAccessorValue(row, xAccessor) === getAccessorValue(geometry.datum, xAccessor) && yAccessor !== null && - getAccessorValue(row, yAccessor) === geometry.y && + getAccessorValue(row, yAccessor) === getAccessorValue(geometry.datum, yAccessor) && (splitChartAccessor === undefined || (splitChartValue !== undefined && getAccessorValue(row, splitChartAccessor) === splitChartValue)))) && diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts index bb95e9e4c38b89..4ebca5ba8965ef 100644 --- a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -147,7 +147,10 @@ function createExtractPanelReferencesMigration( }; } -type ValueOrReferenceInput = SavedObjectEmbeddableInput & { attributes?: SerializableValue }; +type ValueOrReferenceInput = SavedObjectEmbeddableInput & { + attributes?: SerializableValue; + savedVis?: SerializableValue; +}; // Runs the embeddable migrations on each panel const migrateByValuePanels = ( @@ -158,19 +161,21 @@ const migrateByValuePanels = ( // Skip if panelsJSON is missing otherwise this will cause saved object import to fail when // importing objects without panelsJSON. At development time of this, there is no guarantee each saved // object has panelsJSON in all previous versions of kibana. - if (typeof attributes.panelsJSON !== 'string') { - return attributes; + if (typeof attributes?.panelsJSON !== 'string') { + return doc; } const panels = JSON.parse(attributes.panelsJSON) as SavedDashboardPanel[]; // Same here, prevent failing saved object import if ever panels aren't an array. if (!Array.isArray(panels)) { - return attributes; + return doc; } const newPanels: SavedDashboardPanel[] = []; panels.forEach((panel) => { // Convert each panel into a state that can be passed to EmbeddablesSetup.migrate const originalPanelState = convertSavedDashboardPanelToPanelState(panel); - if (originalPanelState.explicitInput.attributes) { + + // saved vis is used to store by value input for Visualize. This should eventually be renamed to `attributes` to align with Lens and Maps + if (originalPanelState.explicitInput.attributes || originalPanelState.explicitInput.savedVis) { // If this panel is by value, migrate the state using embeddable migrations const migratedInput = deps.embeddable.migrate( { diff --git a/src/plugins/data/common/es_query/filters/meta_filter.ts b/src/plugins/data/common/es_query/filters/meta_filter.ts index c47dcb245cbf08..87455cf1cb7639 100644 --- a/src/plugins/data/common/es_query/filters/meta_filter.ts +++ b/src/plugins/data/common/es_query/filters/meta_filter.ts @@ -31,6 +31,7 @@ export type FilterMeta = { controlledBy?: string; // index and type are optional only because when you create a new filter, there are no defaults index?: string; + isMultiIndex?: boolean; type?: string; key?: string; params?: any; diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 04d27851377193..66e66051a6370e 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -192,7 +192,7 @@ export class IndexPatternsService { * Get default index pattern */ getDefault = async () => { - const defaultIndexPatternId = await this.config.get('defaultIndex'); + const defaultIndexPatternId = await this.getDefaultId(); if (defaultIndexPatternId) { return await this.get(defaultIndexPatternId); } @@ -200,12 +200,20 @@ export class IndexPatternsService { return null; }; + /** + * Get default index pattern id + */ + getDefaultId = async (): Promise => { + const defaultIndexPatternId = await this.config.get('defaultIndex'); + return defaultIndexPatternId ?? null; + }; + /** * Optionally set default index pattern, unless force = true * @param id * @param force */ - setDefault = async (id: string, force = false) => { + setDefault = async (id: string | null, force = false) => { if (force || !this.config.get('defaultIndex')) { await this.config.set('defaultIndex', id); } diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index d4ac72294e2571..43445d9448f2c0 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -21,6 +21,9 @@ export interface ApplyGlobalFilterActionContext { // Need to make this unknown to prevent circular dependencies. // Apps using this property will need to cast to `IEmbeddable`. embeddable?: unknown; + // controlledBy is an optional key in filter.meta that identifies the owner of a filter + // Pass controlledBy to cleanup an existing filter(s) owned by embeddable prior to adding new filters + controlledBy?: string; } async function isCompatible(context: ApplyGlobalFilterActionContext) { @@ -42,7 +45,7 @@ export function createFilterAction( }); }, isCompatible, - execute: async ({ filters, timeFieldName }: ApplyGlobalFilterActionContext) => { + execute: async ({ filters, timeFieldName, controlledBy }: ApplyGlobalFilterActionContext) => { if (!filters) { throw new Error('Applying a filter requires a filter'); } @@ -85,6 +88,15 @@ export function createFilterAction( selectedFilters = await filterSelectionPromise; } + // remove existing filters for control prior to adding new filtes for control + if (controlledBy) { + filterManager.getFilters().forEach((filter) => { + if (filter.meta.controlledBy === controlledBy) { + filterManager.removeFilter(filter); + } + }); + } + if (timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( timeFieldName, diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 8561d7bf8d6f5d..fde7075d9e760e 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -492,6 +492,8 @@ export const APPLY_FILTER_TRIGGER = "FILTER_TRIGGER"; // // @public (undocumented) export interface ApplyGlobalFilterActionContext { + // (undocumented) + controlledBy?: string; // (undocumented) embeddable?: unknown; // (undocumented) @@ -763,6 +765,7 @@ export const esFilters: { disabled: boolean; controlledBy?: string | undefined; index?: string | undefined; + isMultiIndex?: boolean | undefined; type?: string | undefined; key?: string | undefined; params?: any; @@ -1621,6 +1624,7 @@ export class IndexPatternsService { // (undocumented) getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; + getDefaultId: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts getFieldsForWildcard: (options: GetFieldsOptions) => Promise; @@ -1632,7 +1636,7 @@ export class IndexPatternsService { getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject) => IndexPatternSpec; - setDefault: (id: string, force?: boolean) => Promise; + setDefault: (id: string | null, force?: boolean) => Promise; updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; } @@ -2689,8 +2693,8 @@ export interface WaitUntilNextSessionCompletesOptions { // src/plugins/data/common/es_query/filters/exists_filter.ts:19:3 - (ae-forgotten-export) The symbol "ExistsFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/exists_filter.ts:20:3 - (ae-forgotten-export) The symbol "FilterExistsProperty" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/match_all_filter.ts:17:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/meta_filter.ts:42:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:22:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:20:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/public/ui/filter_bar/filter_item.tsx b/src/plugins/data/public/ui/filter_bar/filter_item.tsx index 5ad88e6fdf5bef..9e5090f9451829 100644 --- a/src/plugins/data/public/ui/filter_bar/filter_item.tsx +++ b/src/plugins/data/public/ui/filter_bar/filter_item.tsx @@ -286,6 +286,11 @@ export function FilterItem(props: FilterItemProps) { message: '', status: FILTER_ITEM_OK, }; + + if (filter.meta?.isMultiIndex) { + return label; + } + if (indexPatternExists === false) { label.status = FILTER_ITEM_ERROR; label.title = props.intl.formatMessage({ diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index 84199fe60b997b..9bff590b54f1c1 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -20,6 +20,7 @@ import { registerGetScriptedFieldRoute } from './routes/scripted_fields/get_scri import { registerDeleteScriptedFieldRoute } from './routes/scripted_fields/delete_scripted_field'; import { registerUpdateScriptedFieldRoute } from './routes/scripted_fields/update_scripted_field'; import type { DataPluginStart, DataPluginStartDependencies } from '../plugin'; +import { registerManageDefaultIndexPatternRoutes } from './routes/default_index_pattern'; export function registerRoutes( http: HttpServiceSetup, @@ -42,6 +43,7 @@ export function registerRoutes( registerGetIndexPatternRoute(router, getStartServices); registerDeleteIndexPatternRoute(router, getStartServices); registerUpdateIndexPatternRoute(router, getStartServices); + registerManageDefaultIndexPatternRoutes(router, getStartServices); // Fields API registerUpdateFieldsRoute(router, getStartServices); diff --git a/src/plugins/data/server/index_patterns/routes/default_index_pattern.ts b/src/plugins/data/server/index_patterns/routes/default_index_pattern.ts new file mode 100644 index 00000000000000..cf5986943eb373 --- /dev/null +++ b/src/plugins/data/server/index_patterns/routes/default_index_pattern.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { schema } from '@kbn/config-schema'; +import { IRouter, StartServicesAccessor } from '../../../../../core/server'; +import type { DataPluginStart, DataPluginStartDependencies } from '../../plugin'; +import { handleErrors } from './util/handle_errors'; + +export const registerManageDefaultIndexPatternRoutes = ( + router: IRouter, + getStartServices: StartServicesAccessor +) => { + router.get( + { + path: '/api/index_patterns/default', + validate: {}, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + + const defaultIndexPatternId = await indexPatternsService.getDefaultId(); + + return res.ok({ + body: { + index_pattern_id: defaultIndexPatternId, + }, + }); + }) + ); + + router.post( + { + path: '/api/index_patterns/default', + validate: { + body: schema.object({ + index_pattern_id: schema.nullable( + schema.string({ + minLength: 1, + maxLength: 1_000, + }) + ), + force: schema.boolean({ defaultValue: false }), + }), + }, + }, + handleErrors(async (ctx, req, res) => { + const savedObjectsClient = ctx.core.savedObjects.client; + const elasticsearchClient = ctx.core.elasticsearch.client.asCurrentUser; + const [, , { indexPatterns }] = await getStartServices(); + const indexPatternsService = await indexPatterns.indexPatternsServiceFactory( + savedObjectsClient, + elasticsearchClient + ); + + const newDefaultId = req.body.index_pattern_id; + const force = req.body.force; + + await indexPatternsService.setDefault(newDefaultId, force); + + return res.ok({ + body: { + acknowledged: true, + }, + }); + }) + ); +}; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index a8eafd7b14aa98..4abf4302521644 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -962,6 +962,7 @@ class IndexPatternsService { // (undocumented) getCache: () => Promise[] | null | undefined>; getDefault: () => Promise; + getDefaultId: () => Promise; getFieldsForIndexPattern: (indexPattern: IndexPattern | IndexPatternSpec, options?: GetFieldsOptions | undefined) => Promise; // Warning: (ae-forgotten-export) The symbol "GetFieldsOptions" needs to be exported by the entry point index.d.ts getFieldsForWildcard: (options: GetFieldsOptions) => Promise; @@ -973,7 +974,7 @@ class IndexPatternsService { getTitles: (refresh?: boolean) => Promise; refreshFields: (indexPattern: IndexPattern) => Promise; savedObjectToSpec: (savedObject: SavedObject_2) => IndexPatternSpec; - setDefault: (id: string, force?: boolean) => Promise; + setDefault: (id: string | null, force?: boolean) => Promise; updateSavedObject(indexPattern: IndexPattern, saveAttempts?: number, ignoreErrors?: boolean): Promise; } @@ -1511,8 +1512,8 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // Warnings were encountered during analysis: // -// src/plugins/data/common/es_query/filters/meta_filter.ts:42:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts -// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/meta_filter.ts:43:3 - (ae-forgotten-export) The symbol "FilterState" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/es_query/filters/meta_filter.ts:44:3 - (ae-forgotten-export) The symbol "FilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:52:45 - (ae-forgotten-export) The symbol "IndexPatternFieldMap" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:65:5 - (ae-forgotten-export) The symbol "FormatFieldFn" needs to be exported by the entry point index.d.ts // src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts:138:7 - (ae-forgotten-export) The symbol "FieldAttrSet" needs to be exported by the entry point index.d.ts diff --git a/src/plugins/data/server/ui_settings.ts b/src/plugins/data/server/ui_settings.ts index ed3510c5776e0d..e0d475e7124fd2 100644 --- a/src/plugins/data/server/ui_settings.ts +++ b/src/plugins/data/server/ui_settings.ts @@ -605,35 +605,35 @@ export function getUiSettings(): Record> { }), }, { - from: 'now-24h', + from: 'now-24h/h', to: 'now', display: i18n.translate('data.advancedSettings.timepicker.last24Hours', { defaultMessage: 'Last 24 hours', }), }, { - from: 'now-7d', + from: 'now-7d/d', to: 'now', display: i18n.translate('data.advancedSettings.timepicker.last7Days', { defaultMessage: 'Last 7 days', }), }, { - from: 'now-30d', + from: 'now-30d/d', to: 'now', display: i18n.translate('data.advancedSettings.timepicker.last30Days', { defaultMessage: 'Last 30 days', }), }, { - from: 'now-90d', + from: 'now-90d/d', to: 'now', display: i18n.translate('data.advancedSettings.timepicker.last90Days', { defaultMessage: 'Last 90 days', }), }, { - from: 'now-1y', + from: 'now-1y/d', to: 'now', display: i18n.translate('data.advancedSettings.timepicker.last1Year', { defaultMessage: 'Last 1 year', diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx index b3fada3dbd00ff..dfea1a94de7fad 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor/field_editor.test.tsx @@ -14,12 +14,11 @@ import { registerTestBed, TestBed, getCommonActions } from '../../test_utils'; import { RuntimeFieldPainlessError } from '../../lib'; import { Field } from '../../types'; import { FieldEditor, Props, FieldEditorFormState } from './field_editor'; +import { docLinksServiceMock } from '../../../../../core/public/mocks'; const defaultProps: Props = { onChange: jest.fn(), - links: { - runtimePainless: 'https://elastic.co', - }, + links: docLinksServiceMock.createStartContract() as any, ctx: { existingConcreteFields: [], namesNotAllowed: [], diff --git a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts index 286931ad0e8540..ed71e40fc80a9e 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts +++ b/src/plugins/index_pattern_field_editor/public/components/field_editor_flyout_content.test.ts @@ -8,15 +8,16 @@ import { act } from 'react-dom/test-utils'; import '../test_utils/setup_environment'; -import { registerTestBed, TestBed, noop, docLinks, getCommonActions } from '../test_utils'; +import { registerTestBed, TestBed, noop, getCommonActions } from '../test_utils'; import { FieldEditor } from './field_editor'; import { FieldEditorFlyoutContent, Props } from './field_editor_flyout_content'; +import { docLinksServiceMock } from '../../../../core/public/mocks'; const defaultProps: Props = { onSave: noop, onCancel: noop, - docLinks, + docLinks: docLinksServiceMock.createStartContract() as any, FieldEditor, indexPattern: { fields: [] } as any, uiSettings: {} as any, diff --git a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts index 70f180d7cb5f28..a18a75f63d6b8e 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/documentation.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/documentation.ts @@ -8,14 +8,9 @@ import { DocLinksStart } from 'src/core/public'; -export const getLinks = (docLinks: DocLinksStart) => { - const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docLinks; - const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; - const painlessDocsBase = `${docsBase}/elasticsearch/painless/${DOC_LINK_VERSION}`; - const kibanaDocsBase = `${docsBase}/kibana/${DOC_LINK_VERSION}`; - +export const getLinks = ({ links }: DocLinksStart) => { return { - runtimePainless: `${kibanaDocsBase}/managing-index-patterns.html#runtime-fields`, - painlessSyntax: `${painlessDocsBase}/painless-lang-spec.html`, + runtimePainless: links.indexPatterns.runtimeFields, + painlessSyntax: links.scriptedFields.painlessLangSpec, }; }; diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts index 3f39b5563ebc00..bf51e21bb9bf4e 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts @@ -308,6 +308,23 @@ export function getCoreUsageCollector( }, }, }, + + deprecatedKeys: { + set: { + type: 'array', + items: { + type: 'keyword', + _meta: { description: 'Config path added during config deprecation.' }, + }, + }, + unset: { + type: 'array', + items: { + type: 'keyword', + _meta: { description: 'Config path removed during config deprecation.' }, + }, + }, + }, }, environment: { memory: { diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 230d2052f089ed..693957057f1084 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -6950,6 +6950,28 @@ } } } + }, + "deprecatedKeys": { + "properties": { + "set": { + "type": "array", + "items": { + "type": "keyword", + "_meta": { + "description": "Config path added during config deprecation." + } + } + }, + "unset": { + "type": "array", + "items": { + "type": "keyword", + "_meta": { + "description": "Config path removed during config deprecation." + } + } + } + } } } }, diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 3cb4faaacf25be..a2efe39b2c7f0e 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -74,8 +74,11 @@ export const metricsVisDefinition = { showIndexSelection: false, }, toExpressionAst, - getSupportedTriggers: () => { - return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + getSupportedTriggers: (params?: VisParams) => { + if (params?.type === PANEL_TYPES.TIMESERIES) { + return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush]; + } + return []; }, inspectorAdapters: {}, getUsedIndexPattern: async (params: VisParams) => { diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts index 75e3b66a784b3d..04957fda5b8ffb 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/constants.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { Style } from 'mapbox-gl'; +import type { Style } from '@kbn/mapbox-gl'; import { TMS_IN_YML_ID } from '../../../../maps_ems/public'; export const vegaLayerId = 'vega'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts index d3e8bc3f5ab331..428910cbf2d389 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/types.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { Map } from 'mapbox-gl'; +import type { Map } from '@kbn/mapbox-gl'; export interface LayerParameters = {}> { id: string; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts index 8972b80cb99c59..b6bac1e842926e 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/layers/vega_layer.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import type { Map, CustomLayerInterface } from 'mapbox-gl'; +import type { Map, CustomLayerInterface } from '@kbn/mapbox-gl'; import type { View } from 'vega'; import type { LayerParameters } from './types'; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 835ac36ceee471..f4104a0f2457c4 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -7,7 +7,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { Map, Style, MapboxOptions } from 'mapbox-gl'; +import type { Map, Style, MapboxOptions } from '@kbn/mapbox-gl'; import { View, parse } from 'vega'; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index f5bf6b59aa0ae4..03fa8d415aca2a 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -420,7 +420,7 @@ export class VisualizeEmbeddable }; public supportedTriggers(): string[] { - return this.vis.type.getSupportedTriggers?.() ?? []; + return this.vis.type.getSupportedTriggers?.(this.vis.params) ?? []; } inputIsRefType = (input: VisualizeInput): input is VisualizeByReferenceInput => { diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 7cfa31adfca4a0..77654c8a157e36 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -91,7 +91,7 @@ export interface VisTypeDefinition { /** * If given, it will return the supported triggers for this vis. */ - readonly getSupportedTriggers?: () => string[]; + readonly getSupportedTriggers?: (params?: VisParams) => string[]; /** * Some visualizations are created without SearchSource and may change the used indexes during the visualization configuration. diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts new file mode 100644 index 00000000000000..6f214745e12912 --- /dev/null +++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.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 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 { flow } from 'lodash'; +import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; +import { SerializableState } from '../../../kibana_utils/common'; +import { + commonAddSupportOfDualIndexSelectionModeInTSVB, + commonHideTSVBLastValueIndicator, + commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel, +} from '../migrations/visualization_common_migrations'; + +const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableState) => { + return { + ...state, + savedVis: commonAddSupportOfDualIndexSelectionModeInTSVB(state.savedVis), + }; +}; + +const byValueHideTSVBLastValueIndicator = (state: SerializableState) => { + return { + ...state, + savedVis: commonHideTSVBLastValueIndicator(state.savedVis), + }; +}; + +const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: SerializableState) => { + return { + ...state, + savedVis: commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel(state.savedVis), + }; +}; + +export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => { + return { + id: 'visualization', + migrations: { + // These migrations are run in 7.13.1 for `by value` panels because the 7.13 release window was missed. + '7.13.1': (state) => + flow( + byValueAddSupportOfDualIndexSelectionModeInTSVB, + byValueHideTSVBLastValueIndicator, + byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel + )(state), + }, + }; +}; diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts new file mode 100644 index 00000000000000..3f09f19d9ac63a --- /dev/null +++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const commonAddSupportOfDualIndexSelectionModeInTSVB = (visState: any) => { + if (visState && visState.type === 'metrics') { + const { params } = visState; + + if (typeof params?.index_pattern === 'string') { + params.use_kibana_indexes = false; + } + } + return visState; +}; + +export const commonHideTSVBLastValueIndicator = (visState: any) => { + if (visState && visState.type === 'metrics' && visState.params.type !== 'timeseries') { + return { + ...visState, + params: { + ...visState.params, + hide_last_value_indicator: true, + }, + }; + } + return visState; +}; + +export const commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (visState: any) => { + if (visState && visState.type === 'metrics') { + const { params } = visState; + + delete params.default_index_pattern; + delete params.default_timefield; + + return visState; + } + + return visState; +}; diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts similarity index 97% rename from src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts rename to src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index b082737cce288d..dbe5482c442b7c 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { visualizationSavedObjectTypeMigrations } from './visualization_migrations'; +import { visualizationSavedObjectTypeMigrations } from './visualization_saved_object_migrations'; import { SavedObjectMigrationContext, SavedObjectMigrationFn } from 'kibana/server'; const savedObjectMigrationContext = (null as unknown) as SavedObjectMigrationContext; @@ -1977,4 +1977,44 @@ describe('migration visualization', () => { expect(params).not.toHaveProperty('default_timefield'); }); }); + + describe('7.13.0 and 7.13.1 tsvb migrations can run twice', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.13.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const migrateAgain = (doc: any) => + visualizationSavedObjectTypeMigrations['7.13.1']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const createTestDocWithType = (type: string) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: `{"type":"metrics","params":{"type":"${type}","default_index_pattern":"test", "default_timefield":"test", "index_pattern":"testme"}}`, + }, + }); + + it('the migrations can be applied twice without breaking anything', () => { + const migratedTestDoc = migrate(createTestDocWithType('markdown')); + const { params } = JSON.parse(migratedTestDoc.attributes.visState); + + expect(params.hide_last_value_indicator).toBeTruthy(); + expect(params).not.toHaveProperty('default_index_pattern'); + expect(params).not.toHaveProperty('default_timefield'); + expect(params.use_kibana_indexes).toBeFalsy(); + + const migratedTestDocNew = migrateAgain(migratedTestDoc); + const visState = JSON.parse(migratedTestDocNew.attributes.visState); + + expect(visState.params.hide_last_value_indicator).toBeTruthy(); + expect(visState.params).not.toHaveProperty('default_index_pattern'); + expect(visState.params).not.toHaveProperty('default_timefield'); + expect(params.use_kibana_indexes).toBeFalsy(); + }); + }); }); diff --git a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts similarity index 96% rename from src/plugins/visualizations/server/saved_objects/visualization_migrations.ts rename to src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index 093255d65a7a8c..b9885588b6f760 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -11,6 +11,11 @@ import { cloneDeep, get, omit, has, flow, forOwn } from 'lodash'; import { SavedObjectMigrationFn } from 'kibana/server'; import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; +import { + commonAddSupportOfDualIndexSelectionModeInTSVB, + commonHideTSVBLastValueIndicator, + commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel, +} from './visualization_common_migrations'; const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); @@ -799,22 +804,16 @@ const addSupportOfDualIndexSelectionModeInTSVB: SavedObjectMigrationFn visState = JSON.parse(visStateJSON); } catch (e) { // Let it go, the data is invalid and we'll leave it as is + return doc; } - if (visState && visState.type === 'metrics') { - const { params } = visState; - - if (typeof params?.index_pattern === 'string') { - params.use_kibana_indexes = false; - } - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; - } + const newVisState = commonAddSupportOfDualIndexSelectionModeInTSVB(visState); + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(newVisState), + }, + }; } return doc; }; @@ -929,25 +928,17 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn = (doc) => const hideTSVBLastValueIndicator: SavedObjectMigrationFn = (doc) => { try { const visState = JSON.parse(doc.attributes.visState); - - if (visState && visState.type === 'metrics' && visState.params.type !== 'timeseries') - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify({ - ...visState, - params: { - ...visState.params, - hide_last_value_indicator: true, - }, - }), - }, - }; + const newVisState = commonHideTSVBLastValueIndicator(visState); + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(newVisState), + }, + }; } catch (e) { // Let it go, the data is invalid and we'll leave it as is } - return doc; }; @@ -962,23 +953,17 @@ const removeDefaultIndexPatternAndTimeFieldFromTSVBModel: SavedObjectMigrationFn visState = JSON.parse(visStateJSON); } catch (e) { // Let it go, the data is invalid and we'll leave it as is - } - if (visState && visState.type === 'metrics') { - const { params } = visState; - - delete params.default_index_pattern; - delete params.default_timefield; - - return { - ...doc, - attributes: { - ...doc.attributes, - visState: JSON.stringify(visState), - }, - }; + return doc; } } - return doc; + const newVisState = commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel(visState); + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(newVisState), + }, + }; }; export const visualizationSavedObjectTypeMigrations = { @@ -1021,4 +1006,10 @@ export const visualizationSavedObjectTypeMigrations = { hideTSVBLastValueIndicator, removeDefaultIndexPatternAndTimeFieldFromTSVBModel ), + '7.13.1': flow( + // duplicate these migrations in case a broken by value panel is added to the library + addSupportOfDualIndexSelectionModeInTSVB, + hideTSVBLastValueIndicator, + removeDefaultIndexPatternAndTimeFieldFromTSVBModel + ), }; diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts index fa64e28a39dc77..5a5a80b2689d6e 100644 --- a/src/plugins/visualizations/server/plugin.ts +++ b/src/plugins/visualizations/server/plugin.ts @@ -24,6 +24,8 @@ import { visualizationSavedObjectType } from './saved_objects'; import { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; import { registerVisualizationsCollector } from './usage_collector'; +import { EmbeddableSetup } from '../../embeddable/server'; +import { visualizeEmbeddableFactory } from './embeddable/visualize_embeddable_factory'; export class VisualizationsPlugin implements Plugin { @@ -35,7 +37,10 @@ export class VisualizationsPlugin this.config = initializerContext.config.legacy.globalConfig$; } - public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { + public setup( + core: CoreSetup, + plugins: { usageCollection?: UsageCollectionSetup; embeddable: EmbeddableSetup } + ) { this.logger.debug('visualizations: Setup'); core.savedObjects.registerType(visualizationSavedObjectType); @@ -59,6 +64,8 @@ export class VisualizationsPlugin registerVisualizationsCollector(plugins.usageCollection, this.config); } + plugins.embeddable.registerEmbeddableFactory(visualizeEmbeddableFactory()); + return {}; } diff --git a/src/plugins/visualizations/server/saved_objects/visualization.ts b/src/plugins/visualizations/server/saved_objects/visualization.ts index fb7ab3d1531cc6..880e277294fc3a 100644 --- a/src/plugins/visualizations/server/saved_objects/visualization.ts +++ b/src/plugins/visualizations/server/saved_objects/visualization.ts @@ -7,7 +7,7 @@ */ import { SavedObjectsType } from 'kibana/server'; -import { visualizationSavedObjectTypeMigrations } from './visualization_migrations'; +import { visualizationSavedObjectTypeMigrations } from '../migrations/visualization_saved_object_migrations'; export const visualizationSavedObjectType: SavedObjectsType = { name: 'visualization', diff --git a/test/api_integration/apis/index_patterns/default_index_pattern/default_index_pattern.ts b/test/api_integration/apis/index_patterns/default_index_pattern/default_index_pattern.ts new file mode 100644 index 00000000000000..b12600f5ce4f3d --- /dev/null +++ b/test/api_integration/apis/index_patterns/default_index_pattern/default_index_pattern.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('default index pattern api', () => { + const newId = () => `default-id-${Date.now()}-${Math.random()}`; + it('can set default index pattern', async () => { + const defaultId = newId(); + const response1 = await supertest.post('/api/index_patterns/default').send({ + index_pattern_id: defaultId, + force: true, + }); + expect(response1.status).to.be(200); + expect(response1.body.acknowledged).to.be(true); + + const response2 = await supertest.get('/api/index_patterns/default'); + expect(response2.status).to.be(200); + expect(response2.body.index_pattern_id).to.be(defaultId); + + const response3 = await supertest.post('/api/index_patterns/default').send({ + index_pattern_id: newId(), + // no force this time, so this new default shouldn't be set + }); + + expect(response3.status).to.be(200); + const response4 = await supertest.get('/api/index_patterns/default'); + expect(response4.body.index_pattern_id).to.be(defaultId); // original default id is used + + const response5 = await supertest.post('/api/index_patterns/default').send({ + index_pattern_id: null, + force: true, + }); + expect(response5.status).to.be(200); + + const response6 = await supertest.get('/api/index_patterns/default'); + expect(response6.body.index_pattern_id).to.be(null); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/default_index_pattern/index.ts b/test/api_integration/apis/index_patterns/default_index_pattern/index.ts new file mode 100644 index 00000000000000..7517c87ade25bf --- /dev/null +++ b/test/api_integration/apis/index_patterns/default_index_pattern/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('default index pattern', () => { + loadTestFile(require.resolve('./default_index_pattern')); + }); +} diff --git a/test/api_integration/apis/index_patterns/es_errors/errors.js b/test/api_integration/apis/index_patterns/es_errors/errors.js index 3b5b4628f0f20f..fab59ef256a050 100644 --- a/test/api_integration/apis/index_patterns/es_errors/errors.js +++ b/test/api_integration/apis/index_patterns/es_errors/errors.js @@ -7,7 +7,7 @@ */ import expect from '@kbn/expect'; -import { errors as esErrors } from 'elasticsearch'; +import { errors as esErrors } from '@elastic/elasticsearch'; import Boom from '@hapi/boom'; import { @@ -20,7 +20,7 @@ import { import { getIndexNotFoundError, getDocNotFoundError } from './lib'; export default function ({ getService }) { - const es = getService('legacyEs'); + const es = getService('es'); const esArchiver = getService('esArchiver'); describe('index_patterns/* error handler', () => { @@ -98,8 +98,8 @@ export default function ({ getService }) { }); it('wraps other errors in Boom', async () => { - const error = new esErrors.AuthenticationException( - { + const error = new esErrors.ResponseError({ + body: { root_cause: [ { type: 'security_exception', @@ -109,10 +109,8 @@ export default function ({ getService }) { type: 'security_exception', reason: 'action [indices:data/read/field_caps] is unauthorized for user [standard]', }, - { - statusCode: 403, - } - ); + statusCode: 403, + }); expect(error).to.not.have.property('isBoom'); const converted = convertEsError(indices, error); diff --git a/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js b/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js index 047d9f781421e2..c55f8dd8d6d834 100644 --- a/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js +++ b/test/api_integration/apis/index_patterns/es_errors/lib/get_es_errors.js @@ -14,7 +14,7 @@ export async function getIndexNotFoundError(es) { index: 'SHOULD NOT EXIST', }); } catch (err) { - expect(err).to.have.property('status', 404); // sanity check + expect(err).to.have.property('statusCode', 404); // sanity check return err; } @@ -28,7 +28,7 @@ export async function getDocNotFoundError(es) { id: '1234', }); } catch (err) { - expect(err).to.have.property('status', 404); // sanity check + expect(err).to.have.property('statusCode', 404); // sanity check return err; } diff --git a/test/api_integration/apis/index_patterns/index.js b/test/api_integration/apis/index_patterns/index.js index 8d279a57837032..9c1e1bba0ab9a5 100644 --- a/test/api_integration/apis/index_patterns/index.js +++ b/test/api_integration/apis/index_patterns/index.js @@ -14,5 +14,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./index_pattern_crud')); loadTestFile(require.resolve('./scripted_fields_crud')); loadTestFile(require.resolve('./fields_api')); + loadTestFile(require.resolve('./default_index_pattern')); }); } diff --git a/test/api_integration/apis/ui_metric/ui_metric.ts b/test/api_integration/apis/ui_metric/ui_metric.ts index 47d10da9a1b29c..3f0a4c07789117 100644 --- a/test/api_integration/apis/ui_metric/ui_metric.ts +++ b/test/api_integration/apis/ui_metric/ui_metric.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { ReportManager, METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics'; -import { UserAgentMetric } from '@kbn/analytics/target/types/metrics/user_agent'; +import { UserAgentMetric } from '@kbn/analytics/target_types/metrics/user_agent'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index 306d2516293960..d4b49d74d1b901 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -11,7 +11,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - const es = getService('legacyEs'); + const es = getService('es'); const PageObjects = getPageObjects(['settings', 'common', 'header']); const security = getService('security'); diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index b88dfe0f25524e..6c90556d77cf05 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const esArchiver = getService('esArchiver'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const security = getService('security'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); diff --git a/test/functional/apps/management/_handle_version_conflict.js b/test/functional/apps/management/_handle_version_conflict.js index 16c427e9bbe20b..2daad1e457a6be 100644 --- a/test/functional/apps/management/_handle_version_conflict.js +++ b/test/functional/apps/management/_handle_version_conflict.js @@ -21,7 +21,7 @@ export default function ({ getService, getPageObjects }) { const testSubjects = getService('testSubjects'); const esArchiver = getService('esArchiver'); const browser = getService('browser'); - const es = getService('legacyEs'); + const es = getService('es'); const retry = getService('retry'); const scriptedFiledName = 'versionConflictScript'; const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'header']); @@ -49,7 +49,7 @@ export default function ({ getService, getPageObjects }) { }, }); log.debug(JSON.stringify(response)); - expect(response.result).to.be('updated'); + expect(response.body.result).to.be('updated'); await PageObjects.settings.setFieldFormat('url'); await PageObjects.settings.clickSaveScriptedField(); await retry.try(async function () { @@ -80,7 +80,7 @@ export default function ({ getService, getPageObjects }) { }, }); log.debug(JSON.stringify(response)); - expect(response.result).to.be('updated'); + expect(response.body.result).to.be('updated'); await PageObjects.settings.controlChangeSave(); await retry.try(async function () { //await PageObjects.common.sleep(2000); diff --git a/test/functional/apps/management/_index_patterns_empty.ts b/test/functional/apps/management/_index_patterns_empty.ts index 90dd8cdc35c300..3a09340f06ba04 100644 --- a/test/functional/apps/management/_index_patterns_empty.ts +++ b/test/functional/apps/management/_index_patterns_empty.ts @@ -15,7 +15,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'settings']); const testSubjects = getService('testSubjects'); const globalNav = getService('globalNav'); - const es = getService('legacyEs'); + const es = getService('es'); describe('index pattern empty view', () => { before(async () => { @@ -28,7 +28,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { after(async () => { await esArchiver.loadIfNeeded('makelogs'); - // @ts-expect-error await es.transport.request({ path: '/logstash-a', method: 'DELETE', @@ -42,14 +41,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { `\n\nNOTE: If this test fails make sure there aren't any non-system indices in the _cat/indices output (use esArchiver.unload on them)` ); log.debug( - // @ts-expect-error await es.transport.request({ path: '/_cat/indices', method: 'GET', }) ); await testSubjects.existOrFail('createAnyway'); - // @ts-expect-error + await es.transport.request({ path: '/logstash-a/_doc', method: 'POST', diff --git a/test/functional/apps/visualize/_vega_chart.ts b/test/functional/apps/visualize/_vega_chart.ts index da33b390925a4e..b2692c2a00d781 100644 --- a/test/functional/apps/visualize/_vega_chart.ts +++ b/test/functional/apps/visualize/_vega_chart.ts @@ -118,12 +118,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await inspector.openInspectorRequestsView(); for (const getFn of [ - inspector.getOpenRequestDetailRequestButton, - inspector.getOpenRequestDetailResponseButton, - inspector.getOpenRequestStatisticButton, - ]) { + 'getOpenRequestDetailRequestButton', + 'getOpenRequestDetailResponseButton', + 'getOpenRequestStatisticButton', + ] as const) { await retry.try(async () => { - const requestStatisticTab = await getFn(); + const requestStatisticTab = await inspector[getFn](); expect(await requestStatisticTab.isEnabled()).to.be(true); }); @@ -159,12 +159,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await vegaDebugInspectorView.openVegaDebugInspectorView(); for (const getFn of [ - vegaDebugInspectorView.getOpenDataViewerButton, - vegaDebugInspectorView.getOpenSignalViewerButton, - vegaDebugInspectorView.getOpenSpecViewerButton, - ]) { + 'getOpenDataViewerButton', + 'getOpenSignalViewerButton', + 'getOpenSpecViewerButton', + ] as const) { await retry.try(async () => { - const requestStatisticTab = await getFn(); + const requestStatisticTab = await vegaDebugInspectorView[getFn](); expect(await requestStatisticTab.isEnabled()).to.be(true); }); diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts index d3b6edaffdbd32..4d0930c3ff932d 100644 --- a/test/functional/page_objects/time_picker.ts +++ b/test/functional/page_objects/time_picker.ts @@ -30,9 +30,9 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo const testSubjects = getService('testSubjects'); const { header } = getPageObjects(['header']); const kibanaServer = getService('kibanaServer'); - const MenuToggle = getService('MenuToggle'); + const menuToggle = getService('menuToggle'); - const quickSelectTimeMenuToggle = new MenuToggle({ + const quickSelectTimeMenuToggle = menuToggle.create({ name: 'QuickSelectTime Menu', menuTestSubject: 'superDatePickerQuickMenu', toggleButtonTestSubject: 'superDatePickerToggleQuickMenuButton', diff --git a/test/functional/services/apps_menu.ts b/test/functional/services/apps_menu.ts index f9c80a6450c2fc..9fb8e36476f3ee 100644 --- a/test/functional/services/apps_menu.ts +++ b/test/functional/services/apps_menu.ts @@ -6,133 +6,132 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function AppsMenuProvider({ getService, getPageObjects }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const log = getService('log'); - const config = getService('config'); - const defaultFindTimeout = config.get('timeouts.find'); - const find = getService('find'); +export class AppsMenuService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly log = this.ctx.getService('log'); + private readonly config = this.ctx.getService('config'); + private readonly find = this.ctx.getService('find'); - return new (class AppsMenu { - private async waitUntilLoadingHasFinished() { - try { - await this.isGlobalLoadingIndicatorVisible(); - } catch (exception) { - if (exception.name === 'ElementNotVisible') { - // selenium might just have been too slow to catch it - } else { - throw exception; - } + private readonly defaultFindTimeout = this.config.get('timeouts.find'); + + private async waitUntilLoadingHasFinished() { + try { + await this.isGlobalLoadingIndicatorVisible(); + } catch (exception) { + if (exception.name === 'ElementNotVisible') { + // selenium might just have been too slow to catch it + } else { + throw exception; } - await this.awaitGlobalLoadingIndicatorHidden(); } + await this.awaitGlobalLoadingIndicatorHidden(); + } - private async isGlobalLoadingIndicatorVisible() { - log.debug('isGlobalLoadingIndicatorVisible'); - return await testSubjects.exists('globalLoadingIndicator', { timeout: 1500 }); - } + private async isGlobalLoadingIndicatorVisible() { + this.log.debug('isGlobalLoadingIndicatorVisible'); + return await this.testSubjects.exists('globalLoadingIndicator', { timeout: 1500 }); + } - private async awaitGlobalLoadingIndicatorHidden() { - await testSubjects.existOrFail('globalLoadingIndicator-hidden', { - allowHidden: true, - timeout: defaultFindTimeout * 10, - }); - } - /** - * Close the collapsible nav - * TODO #64541 can replace with a data-test-subj - */ - public async closeCollapsibleNav() { - const CLOSE_BUTTON = '[data-test-subj=collapsibleNav] > button'; - if (await find.existsByCssSelector(CLOSE_BUTTON)) { - // Close button is only visible when focused - const button = await find.byCssSelector(CLOSE_BUTTON); - await button.focus(); + private async awaitGlobalLoadingIndicatorHidden() { + await this.testSubjects.existOrFail('globalLoadingIndicator-hidden', { + allowHidden: true, + timeout: this.defaultFindTimeout * 10, + }); + } + /** + * Close the collapsible nav + * TODO #64541 can replace with a data-test-subj + */ + public async closeCollapsibleNav() { + const CLOSE_BUTTON = '[data-test-subj=collapsibleNav] > button'; + if (await this.find.existsByCssSelector(CLOSE_BUTTON)) { + // Close button is only visible when focused + const button = await this.find.byCssSelector(CLOSE_BUTTON); + await button.focus(); - await find.clickByCssSelector(CLOSE_BUTTON); - } + await this.find.clickByCssSelector(CLOSE_BUTTON); } + } - public async openCollapsibleNav() { - if (!(await testSubjects.exists('collapsibleNav'))) { - await testSubjects.click('toggleNavButton'); - } + public async openCollapsibleNav() { + if (!(await this.testSubjects.exists('collapsibleNav'))) { + await this.testSubjects.click('toggleNavButton'); } + } - /** - * Get the attributes from each of the links in the apps menu - */ - public async readLinks() { - // wait for the chrome to finish initializing - await this.waitUntilLoadingHasFinished(); - await this.openCollapsibleNav(); - const appMenu = await testSubjects.find('collapsibleNav'); - const $ = await appMenu.parseDomContent(); - const links = $.findTestSubjects('collapsibleNavAppLink') - .toArray() - .map((link) => { - return { - text: $(link).text(), - href: $(link).attr('href'), - disabled: $(link).attr('disabled') != null, - }; - }); + /** + * Get the attributes from each of the links in the apps menu + */ + public async readLinks() { + // wait for the chrome to finish initializing + await this.waitUntilLoadingHasFinished(); + await this.openCollapsibleNav(); + const appMenu = await this.testSubjects.find('collapsibleNav'); + const $ = await appMenu.parseDomContent(); + const links = $.findTestSubjects('collapsibleNavAppLink') + .toArray() + .map((link) => { + return { + text: $(link).text(), + href: $(link).attr('href'), + disabled: $(link).attr('disabled') != null, + }; + }); - await this.closeCollapsibleNav(); + await this.closeCollapsibleNav(); - return links; - } + return links; + } - /** - * Get the attributes from the link with the given name. - * @param name - */ - public async getLink(name: string) { - return (await this.readLinks()).find((nl) => nl.text === name); - } + /** + * Get the attributes from the link with the given name. + * @param name + */ + public async getLink(name: string) { + return (await this.readLinks()).find((nl) => nl.text === name); + } - /** - * Determine if an app link with the given name exists - * @param name - */ - public async linkExists(name: string) { - return (await this.readLinks()).some((nl) => nl.text === name); - } + /** + * Determine if an app link with the given name exists + * @param name + */ + public async linkExists(name: string) { + return (await this.readLinks()).some((nl) => nl.text === name); + } - /** - * Click the app link within the app menu that has the given name - * @param name - * @param options.closeCollapsibleNav - * @param options.category - optional field to ensure that a link is clicked in a particular category - * helpful when there may be a recent link with the same name as an app - */ - public async clickLink( - name: string, - { - closeCollapsibleNav = true, - category, - }: { closeCollapsibleNav?: boolean; category?: string } = {} - ) { - try { - log.debug(`click "${name}" app link`); - await this.openCollapsibleNav(); - let nav; - if (typeof category === 'string') { - nav = await testSubjects.find(`collapsibleNavGroup-${category}`); - } else { - nav = await testSubjects.find('collapsibleNav'); - } - const link = await nav.findByPartialLinkText(name); - await link.click(); + /** + * Click the app link within the app menu that has the given name + * @param name + * @param options.closeCollapsibleNav + * @param options.category - optional field to ensure that a link is clicked in a particular category + * helpful when there may be a recent link with the same name as an app + */ + public async clickLink( + name: string, + { + closeCollapsibleNav = true, + category, + }: { closeCollapsibleNav?: boolean; category?: string } = {} + ) { + try { + this.log.debug(`click "${name}" app link`); + await this.openCollapsibleNav(); + let nav; + if (typeof category === 'string') { + nav = await this.testSubjects.find(`collapsibleNavGroup-${category}`); + } else { + nav = await this.testSubjects.find('collapsibleNav'); + } + const link = await nav.findByPartialLinkText(name); + await link.click(); - if (closeCollapsibleNav) { - await this.closeCollapsibleNav(); - } - } finally { - // Intentionally empty + if (closeCollapsibleNav) { + await this.closeCollapsibleNav(); } + } finally { + // Intentionally empty } - })(); + } } diff --git a/test/functional/services/combo_box.ts b/test/functional/services/combo_box.ts index 4615a7378415cf..a198aec1d16960 100644 --- a/test/functional/services/combo_box.ts +++ b/test/functional/services/combo_box.ts @@ -6,301 +6,301 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; import { WebElementWrapper } from './lib/web_element_wrapper'; // @ts-ignore not supported yet import { scrollIntoViewIfNecessary } from './lib/web_element_wrapper/scroll_into_view_if_necessary'; -export function ComboBoxProvider({ getService, getPageObjects }: FtrProviderContext) { - const config = getService('config'); - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const log = getService('log'); - const retry = getService('retry'); - const browser = getService('browser'); - const PageObjects = getPageObjects(['common']); +/** + * wrapper around EuiComboBox interactions + */ +export class ComboBoxService extends FtrService { + private readonly config = this.ctx.getService('config'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly find = this.ctx.getService('find'); + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly browser = this.ctx.getService('browser'); + private readonly PageObjects = this.ctx.getPageObjects(['common']); - const WAIT_FOR_EXISTS_TIME: number = config.get('timeouts.waitForExists'); + private readonly WAIT_FOR_EXISTS_TIME: number = this.config.get('timeouts.waitForExists'); - // wrapper around EuiComboBox interactions - class ComboBox { - /** - * Finds combobox element and sets specified value - * - * @param comboBoxSelector data-test-subj selector - * @param value option text - */ + /** + * Finds combobox element and sets specified value + * + * @param comboBoxSelector data-test-subj selector + * @param value option text + */ - public async set(comboBoxSelector: string, value: string): Promise { - log.debug(`comboBox.set, comboBoxSelector: ${comboBoxSelector}`); - const comboBox = await testSubjects.find(comboBoxSelector); - await this.setElement(comboBox, value); - } + public async set(comboBoxSelector: string, value: string): Promise { + this.log.debug(`comboBox.set, comboBoxSelector: ${comboBoxSelector}`); + const comboBox = await this.testSubjects.find(comboBoxSelector); + await this.setElement(comboBox, value); + } - /** - * Clicks option in combobox dropdown - * - * @param isMouseClick if 'true', click will be done with mouse - * @param element element that wraps up option - */ - private async clickOption(isMouseClick: boolean, element: WebElementWrapper): Promise { - // element.click causes scrollIntoView which causes combobox to close, using _webElement.click instead - return isMouseClick ? await element.clickMouseButton() : await element._webElement.click(); - } + /** + * Clicks option in combobox dropdown + * + * @param isMouseClick if 'true', click will be done with mouse + * @param element element that wraps up option + */ + private async clickOption(isMouseClick: boolean, element: WebElementWrapper): Promise { + // element.click causes scrollIntoView which causes combobox to close, using _webElement.click instead + return isMouseClick ? await element.clickMouseButton() : await element._webElement.click(); + } - /** - * Finds combobox element options - * - * @param comboBoxSelector data-test-subj selector - */ - public async getOptions(comboBoxSelector: string) { - const comboBoxElement = await testSubjects.find(comboBoxSelector); - await this.openOptionsList(comboBoxElement); - return await find.allByCssSelector('.euiFilterSelectItem', WAIT_FOR_EXISTS_TIME); - } + /** + * Finds combobox element options + * + * @param comboBoxSelector data-test-subj selector + */ + public async getOptions(comboBoxSelector: string) { + const comboBoxElement = await this.testSubjects.find(comboBoxSelector); + await this.openOptionsList(comboBoxElement); + return await this.find.allByCssSelector('.euiFilterSelectItem', this.WAIT_FOR_EXISTS_TIME); + } - /** - * Sets value for specified combobox element - * - * @param comboBoxElement element that wraps up EuiComboBox - * @param value - */ - public async setElement( - comboBoxElement: WebElementWrapper, - value: string, - options = { clickWithMouse: false } - ): Promise { - log.debug(`comboBox.setElement, value: ${value}`); - const isOptionSelected = await this.isOptionSelected(comboBoxElement, value); + /** + * Sets value for specified combobox element + * + * @param comboBoxElement element that wraps up EuiComboBox + * @param value + */ + public async setElement( + comboBoxElement: WebElementWrapper, + value: string, + options = { clickWithMouse: false } + ): Promise { + this.log.debug(`comboBox.setElement, value: ${value}`); + const isOptionSelected = await this.isOptionSelected(comboBoxElement, value); - if (isOptionSelected) { - return; - } + if (isOptionSelected) { + return; + } - await comboBoxElement.scrollIntoViewIfNecessary(); - await this.setFilterValue(comboBoxElement, value); - await this.openOptionsList(comboBoxElement); + await comboBoxElement.scrollIntoViewIfNecessary(); + await this.setFilterValue(comboBoxElement, value); + await this.openOptionsList(comboBoxElement); - if (value !== undefined) { - const selectOptions = await find.allByCssSelector( - `.euiFilterSelectItem[title^="${value.toString().trim()}"]`, - WAIT_FOR_EXISTS_TIME - ); + if (value !== undefined) { + const selectOptions = await this.find.allByCssSelector( + `.euiFilterSelectItem[title^="${value.toString().trim()}"]`, + this.WAIT_FOR_EXISTS_TIME + ); - if (selectOptions.length > 0) { - await this.clickOption(options.clickWithMouse, selectOptions[0]); - } else { - // if it doesn't find the item which text starts with value, it will choose the first option - const firstOption = await find.byCssSelector('.euiFilterSelectItem', 5000); - await this.clickOption(options.clickWithMouse, firstOption); - } + if (selectOptions.length > 0) { + await this.clickOption(options.clickWithMouse, selectOptions[0]); } else { - const firstOption = await find.byCssSelector('.euiFilterSelectItem'); + // if it doesn't find the item which text starts with value, it will choose the first option + const firstOption = await this.find.byCssSelector('.euiFilterSelectItem', 5000); await this.clickOption(options.clickWithMouse, firstOption); } - await this.closeOptionsList(comboBoxElement); - } - - /** - * Finds combobox element and sets custom value - * It applies changes by pressing Enter key. Sometimes it may lead to auto-submitting a form. - * - * @param comboBoxSelector data-test-subj selector - * @param value option text - */ - public async setCustom(comboBoxSelector: string, value: string): Promise { - log.debug(`comboBox.setCustom, comboBoxSelector: ${comboBoxSelector}, value: ${value}`); - const comboBoxElement = await testSubjects.find(comboBoxSelector); - await this.setFilterValue(comboBoxElement, value); - await PageObjects.common.pressEnterKey(); - await this.closeOptionsList(comboBoxElement); + } else { + const firstOption = await this.find.byCssSelector('.euiFilterSelectItem'); + await this.clickOption(options.clickWithMouse, firstOption); } + await this.closeOptionsList(comboBoxElement); + } - /** - * Finds combobox element and sets filter value - * - * @param comboBoxSelector data-test-subj selector - * @param filterValue text - */ - public async filterOptionsList(comboBoxSelector: string, filterValue: string): Promise { - log.debug( - `comboBox.filterOptionsList, comboBoxSelector: ${comboBoxSelector}, filter: ${filterValue}` - ); - const comboBox = await testSubjects.find(comboBoxSelector); - await this.setFilterValue(comboBox, filterValue); - await this.closeOptionsList(comboBox); - } + /** + * Finds combobox element and sets custom value + * It applies changes by pressing Enter key. Sometimes it may lead to auto-submitting a form. + * + * @param comboBoxSelector data-test-subj selector + * @param value option text + */ + public async setCustom(comboBoxSelector: string, value: string): Promise { + this.log.debug(`comboBox.setCustom, comboBoxSelector: ${comboBoxSelector}, value: ${value}`); + const comboBoxElement = await this.testSubjects.find(comboBoxSelector); + await this.setFilterValue(comboBoxElement, value); + await this.PageObjects.common.pressEnterKey(); + await this.closeOptionsList(comboBoxElement); + } - /** - * Sets new filter value in specified combobox element - * - * @param comboBoxElement element that wraps up EuiComboBox - * @param filterValue text - */ - private async setFilterValue( - comboBoxElement: WebElementWrapper, - filterValue: string - ): Promise { - const input = await comboBoxElement.findByTagName('input'); - await input.clearValue(); - await this.waitForOptionsListLoading(comboBoxElement); - await input.type(filterValue); - await this.waitForOptionsListLoading(comboBoxElement); - } + /** + * Finds combobox element and sets filter value + * + * @param comboBoxSelector data-test-subj selector + * @param filterValue text + */ + public async filterOptionsList(comboBoxSelector: string, filterValue: string): Promise { + this.log.debug( + `comboBox.filterOptionsList, comboBoxSelector: ${comboBoxSelector}, filter: ${filterValue}` + ); + const comboBox = await this.testSubjects.find(comboBoxSelector); + await this.setFilterValue(comboBox, filterValue); + await this.closeOptionsList(comboBox); + } - /** - * Waits options list to be loaded - * - * @param comboBoxElement element that wraps up EuiComboBox - */ - private async waitForOptionsListLoading(comboBoxElement: WebElementWrapper): Promise { - await comboBoxElement.waitForDeletedByCssSelector('.euiLoadingSpinner'); - } + /** + * Sets new filter value in specified combobox element + * + * @param comboBoxElement element that wraps up EuiComboBox + * @param filterValue text + */ + private async setFilterValue( + comboBoxElement: WebElementWrapper, + filterValue: string + ): Promise { + const input = await comboBoxElement.findByTagName('input'); + await input.clearValue(); + await this.waitForOptionsListLoading(comboBoxElement); + await input.type(filterValue); + await this.waitForOptionsListLoading(comboBoxElement); + } - /** - * Returns options list as a single string - * - * @param comboBoxSelector data-test-subj selector - */ - public async getOptionsList(comboBoxSelector: string): Promise { - log.debug(`comboBox.getOptionsList, comboBoxSelector: ${comboBoxSelector}`); - const comboBox = await testSubjects.find(comboBoxSelector); - const menu = await retry.try(async () => { - await testSubjects.click(comboBoxSelector); - await this.waitForOptionsListLoading(comboBox); - const isOptionsListOpen = await testSubjects.exists('~comboBoxOptionsList'); - if (!isOptionsListOpen) { - throw new Error('Combo box options list did not open on click'); - } - return await testSubjects.find('~comboBoxOptionsList'); - }); - const optionsText = await menu.getVisibleText(); - await this.closeOptionsList(comboBox); - return optionsText; - } + /** + * Waits options list to be loaded + * + * @param comboBoxElement element that wraps up EuiComboBox + */ + private async waitForOptionsListLoading(comboBoxElement: WebElementWrapper): Promise { + await comboBoxElement.waitForDeletedByCssSelector('.euiLoadingSpinner'); + } - /** - * Finds combobox element and checks if it has selected options - * - * @param comboBoxSelector data-test-subj selector - */ - public async doesComboBoxHaveSelectedOptions(comboBoxSelector: string): Promise { - log.debug(`comboBox.doesComboBoxHaveSelectedOptions, comboBoxSelector: ${comboBoxSelector}`); - const comboBox = await testSubjects.find(comboBoxSelector); - const $ = await comboBox.parseDomContent(); - return $('.euiComboBoxPill').toArray().length > 0; - } + /** + * Returns options list as a single string + * + * @param comboBoxSelector data-test-subj selector + */ + public async getOptionsList(comboBoxSelector: string): Promise { + this.log.debug(`comboBox.getOptionsList, comboBoxSelector: ${comboBoxSelector}`); + const comboBox = await this.testSubjects.find(comboBoxSelector); + const menu = await this.retry.try(async () => { + await this.testSubjects.click(comboBoxSelector); + await this.waitForOptionsListLoading(comboBox); + const isOptionsListOpen = await this.testSubjects.exists('~comboBoxOptionsList'); + if (!isOptionsListOpen) { + throw new Error('Combo box options list did not open on click'); + } + return await this.testSubjects.find('~comboBoxOptionsList'); + }); + const optionsText = await menu.getVisibleText(); + await this.closeOptionsList(comboBox); + return optionsText; + } - /** - * Returns selected options - * @param comboBoxSelector data-test-subj selector - */ - public async getComboBoxSelectedOptions(comboBoxSelector: string): Promise { - log.debug(`comboBox.getComboBoxSelectedOptions, comboBoxSelector: ${comboBoxSelector}`); - const comboBox = await testSubjects.find(comboBoxSelector); - const $ = await comboBox.parseDomContent(); - return $('.euiComboBoxPill') - .toArray() - .map((option) => $(option).text()); - } + /** + * Finds combobox element and checks if it has selected options + * + * @param comboBoxSelector data-test-subj selector + */ + public async doesComboBoxHaveSelectedOptions(comboBoxSelector: string): Promise { + this.log.debug( + `comboBox.doesComboBoxHaveSelectedOptions, comboBoxSelector: ${comboBoxSelector}` + ); + const comboBox = await this.testSubjects.find(comboBoxSelector); + const $ = await comboBox.parseDomContent(); + return $('.euiComboBoxPill').toArray().length > 0; + } - /** - * Finds combobox element and clears value in the input field by clicking clear button - * - * @param comboBoxSelector data-test-subj selector - */ - public async clear(comboBoxSelector: string): Promise { - log.debug(`comboBox.clear, comboBoxSelector:${comboBoxSelector}`); - const comboBox = await testSubjects.find(comboBoxSelector); - await retry.try(async () => { - const clearButtonExists = await this.doesClearButtonExist(comboBox); - if (!clearButtonExists) { - log.debug('Unable to clear comboBox, comboBoxClearButton does not exist'); - return; - } + /** + * Returns selected options + * @param comboBoxSelector data-test-subj selector + */ + public async getComboBoxSelectedOptions(comboBoxSelector: string): Promise { + this.log.debug(`comboBox.getComboBoxSelectedOptions, comboBoxSelector: ${comboBoxSelector}`); + const comboBox = await this.testSubjects.find(comboBoxSelector); + const $ = await comboBox.parseDomContent(); + return $('.euiComboBoxPill') + .toArray() + .map((option) => $(option).text()); + } - const clearBtn = await comboBox.findByTestSubject('comboBoxClearButton'); - await clearBtn.click(); + /** + * Finds combobox element and clears value in the input field by clicking clear button + * + * @param comboBoxSelector data-test-subj selector + */ + public async clear(comboBoxSelector: string): Promise { + this.log.debug(`comboBox.clear, comboBoxSelector:${comboBoxSelector}`); + const comboBox = await this.testSubjects.find(comboBoxSelector); + await this.retry.try(async () => { + const clearButtonExists = await this.doesClearButtonExist(comboBox); + if (!clearButtonExists) { + this.log.debug('Unable to clear comboBox, comboBoxClearButton does not exist'); + return; + } - const clearButtonStillExists = await this.doesClearButtonExist(comboBox); - if (clearButtonStillExists) { - throw new Error('Failed to clear comboBox'); - } - }); - await this.closeOptionsList(comboBox); - } + const clearBtn = await comboBox.findByTestSubject('comboBoxClearButton'); + await clearBtn.click(); - public async doesClearButtonExist(comboBoxElement: WebElementWrapper): Promise { - const found = await comboBoxElement.findAllByTestSubject( - 'comboBoxClearButton', - WAIT_FOR_EXISTS_TIME - ); - return found.length > 0; - } + const clearButtonStillExists = await this.doesClearButtonExist(comboBox); + if (clearButtonStillExists) { + throw new Error('Failed to clear comboBox'); + } + }); + await this.closeOptionsList(comboBox); + } - public async checkValidity(comboBoxElement: WebElementWrapper): Promise { - const invalidClassName = 'euiComboBox-isInvalid'; + public async doesClearButtonExist(comboBoxElement: WebElementWrapper): Promise { + const found = await comboBoxElement.findAllByTestSubject( + 'comboBoxClearButton', + this.WAIT_FOR_EXISTS_TIME + ); + return found.length > 0; + } - return !(await comboBoxElement.elementHasClass(invalidClassName)); - } + public async checkValidity(comboBoxElement: WebElementWrapper): Promise { + const invalidClassName = 'euiComboBox-isInvalid'; - /** - * Closes options list - * - * @param comboBoxElement element that wraps up EuiComboBox - */ - public async closeOptionsList(comboBoxElement: WebElementWrapper): Promise { - const isOptionsListOpen = await testSubjects.exists('~comboBoxOptionsList'); - if (isOptionsListOpen) { - const input = await comboBoxElement.findByTagName('input'); - await input.pressKeys(browser.keys.ESCAPE); - } - } + return !(await comboBoxElement.elementHasClass(invalidClassName)); + } - /** - * Opens options list - * - * @param comboBoxElement element that wraps up EuiComboBox - */ - public async openOptionsList(comboBoxElement: WebElementWrapper): Promise { - const isOptionsListOpen = await testSubjects.exists('~comboBoxOptionsList'); - if (!isOptionsListOpen) { - await retry.try(async () => { - const toggleBtn = await comboBoxElement.findByTestSubject('comboBoxInput'); - await toggleBtn.click(); - }); - } + /** + * Closes options list + * + * @param comboBoxElement element that wraps up EuiComboBox + */ + public async closeOptionsList(comboBoxElement: WebElementWrapper): Promise { + const isOptionsListOpen = await this.testSubjects.exists('~comboBoxOptionsList'); + if (isOptionsListOpen) { + const input = await comboBoxElement.findByTagName('input'); + await input.pressKeys(this.browser.keys.ESCAPE); } + } - /** - * Checks if specified option is already selected - * - * @param comboBoxElement element that wraps up EuiComboBox - * @param value option text - */ - public async isOptionSelected( - comboBoxElement: WebElementWrapper, - value: string - ): Promise { - log.debug(`comboBox.isOptionSelected, value: ${value}`); - const $ = await comboBoxElement.parseDomContent(); - const selectedOptions = $('.euiComboBoxPill') - .toArray() - .map((option) => $(option).text()); - return selectedOptions.length === 1 && selectedOptions[0] === value; + /** + * Opens options list + * + * @param comboBoxElement element that wraps up EuiComboBox + */ + public async openOptionsList(comboBoxElement: WebElementWrapper): Promise { + const isOptionsListOpen = await this.testSubjects.exists('~comboBoxOptionsList'); + if (!isOptionsListOpen) { + await this.retry.try(async () => { + const toggleBtn = await comboBoxElement.findByTestSubject('comboBoxInput'); + await toggleBtn.click(); + }); } + } - /** - * Clears input field - * @param comboBoxSelector data-test-subj selector - */ - public async clearInputField(comboBoxSelector: string): Promise { - log.debug(`comboBox.clearInputField, comboBoxSelector:${comboBoxSelector}`); - const comboBoxElement = await testSubjects.find(comboBoxSelector); - const input = await comboBoxElement.findByTagName('input'); - await input.clearValueWithKeyboard(); - } + /** + * Checks if specified option is already selected + * + * @param comboBoxElement element that wraps up EuiComboBox + * @param value option text + */ + public async isOptionSelected( + comboBoxElement: WebElementWrapper, + value: string + ): Promise { + this.log.debug(`comboBox.isOptionSelected, value: ${value}`); + const $ = await comboBoxElement.parseDomContent(); + const selectedOptions = $('.euiComboBoxPill') + .toArray() + .map((option) => $(option).text()); + return selectedOptions.length === 1 && selectedOptions[0] === value; } - return new ComboBox(); + /** + * Clears input field + * @param comboBoxSelector data-test-subj selector + */ + public async clearInputField(comboBoxSelector: string): Promise { + this.log.debug(`comboBox.clearInputField, comboBoxSelector:${comboBoxSelector}`); + const comboBoxElement = await this.testSubjects.find(comboBoxSelector); + const input = await comboBoxElement.findByTagName('input'); + await input.clearValueWithKeyboard(); + } } diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts index 4dfd30c3b3b68f..d38203d5d07d36 100644 --- a/test/functional/services/common/browser.ts +++ b/test/functional/services/common/browser.ts @@ -8,586 +8,585 @@ import { delay } from 'bluebird'; import { cloneDeepWith } from 'lodash'; -import { Key, Origin } from 'selenium-webdriver'; +import { Key, Origin, WebDriver } from 'selenium-webdriver'; // @ts-ignore internal modules are not typed import { LegacyActionSequence } from 'selenium-webdriver/lib/actions'; -import { ProvidedType } from '@kbn/test'; import { modifyUrl } from '@kbn/std'; import Jimp from 'jimp'; import { WebElementWrapper } from '../lib/web_element_wrapper'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext, FtrService } from '../../ftr_provider_context'; import { Browsers } from '../remote/browsers'; -export type Browser = ProvidedType; -export async function BrowserProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const { driver, browserType } = await getService('__webdriver__').init(); - - return new (class BrowserService { - /** - * Keyboard events - */ - public readonly keys = Key; - - /** - * Browser name - */ - public readonly browserType: string = browserType; - - public readonly isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes( - browserType +export type Browser = BrowserService; + +class BrowserService extends FtrService { + /** + * Keyboard events + */ + public readonly keys = Key; + public readonly isFirefox: boolean = this.browserType === Browsers.Firefox; + public readonly isChromium: boolean = + this.browserType === Browsers.Chrome || this.browserType === Browsers.ChromiumEdge; + + private readonly log = this.ctx.getService('log'); + + constructor( + ctx: FtrProviderContext, + public readonly browserType: string, + private readonly driver: WebDriver + ) { + super(ctx); + } + + /** + * Returns instance of Actions API based on driver w3c flag + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#actions + */ + public getActions() { + return this.driver.actions(); + } + + /** + * Get handle for an alert, confirm, or prompt dialog. (if any). + * @return {Promise} + */ + public async getAlert() { + try { + return await this.driver.switchTo().alert(); + } catch (e) { + return null; + } + } + + /** + * Retrieves the a rect describing the current top-level window's size and position. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html + * + * @return {Promise<{height: number, width: number, x: number, y: number}>} + */ + public async getWindowSize(): Promise<{ height: number; width: number; x: number; y: number }> { + return await this.driver.manage().window().getRect(); + } + + /** + * Sets the dimensions of a window. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html + * + * @param {number} width + * @param {number} height + * @return {Promise} + */ + public async setWindowSize(width: number, height: number) { + await this.driver.manage().window().setRect({ width, height }); + } + + /** + * Gets a screenshot of the focused window and returns it as a Bitmap object + */ + public async getScreenshotAsBitmap() { + const screenshot = await this.takeScreenshot(); + const buffer = Buffer.from(screenshot, 'base64'); + const session = (await Jimp.read(buffer)).clone(); + return session.bitmap; + } + + /** + * Sets the dimensions of a window to get the right size screenshot. + * + * @param {number} width + * @param {number} height + * @return {Promise} + */ + public async setScreenshotSize(width: number, height: number) { + this.log.debug(`======browser======== setWindowSize ${width} ${height}`); + // We really want to set the Kibana app to a specific size without regard to the browser chrome (borders) + // But that means we first need to figure out the display scaling factor. + // NOTE: None of this is required when running Chrome headless because there's no scaling and no borders. + await this.setWindowSize(1200, 800); + const bitmap1 = await this.getScreenshotAsBitmap(); + this.log.debug( + `======browser======== actual initial screenshot size width=${bitmap1.width}, height=${bitmap1.height}` ); - public readonly isFirefox: boolean = browserType === Browsers.Firefox; - - /** - * Returns instance of Actions API based on driver w3c flag - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#actions - */ - public getActions() { - return driver.actions(); - } - - /** - * Get handle for an alert, confirm, or prompt dialog. (if any). - * @return {Promise} - */ - public async getAlert() { - try { - return await driver.switchTo().alert(); - } catch (e) { - return null; - } - } - - /** - * Retrieves the a rect describing the current top-level window's size and position. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html - * - * @return {Promise<{height: number, width: number, x: number, y: number}>} - */ - public async getWindowSize(): Promise<{ height: number; width: number; x: number; y: number }> { - return await driver.manage().window().getRect(); - } - - /** - * Sets the dimensions of a window. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Window.html - * - * @param {number} width - * @param {number} height - * @return {Promise} - */ - public async setWindowSize(width: number, height: number) { - await driver.manage().window().setRect({ width, height }); - } - - /** - * Gets a screenshot of the focused window and returns it as a Bitmap object - */ - public async getScreenshotAsBitmap() { - const screenshot = await this.takeScreenshot(); - const buffer = Buffer.from(screenshot, 'base64'); - const session = (await Jimp.read(buffer)).clone(); - return session.bitmap; - } + // drasticly change the window size so we can calculate the scaling + await this.setWindowSize(600, 400); + const bitmap2 = await this.getScreenshotAsBitmap(); + this.log.debug( + `======browser======== actual second screenshot size width= ${bitmap2.width}, height=${bitmap2.height}` + ); - /** - * Sets the dimensions of a window to get the right size screenshot. - * - * @param {number} width - * @param {number} height - * @return {Promise} - */ - public async setScreenshotSize(width: number, height: number) { - log.debug(`======browser======== setWindowSize ${width} ${height}`); - // We really want to set the Kibana app to a specific size without regard to the browser chrome (borders) - // But that means we first need to figure out the display scaling factor. - // NOTE: None of this is required when running Chrome headless because there's no scaling and no borders. - await this.setWindowSize(1200, 800); - const bitmap1 = await this.getScreenshotAsBitmap(); - log.debug( - `======browser======== actual initial screenshot size width=${bitmap1.width}, height=${bitmap1.height}` - ); - - // drasticly change the window size so we can calculate the scaling - await this.setWindowSize(600, 400); - const bitmap2 = await this.getScreenshotAsBitmap(); - log.debug( - `======browser======== actual second screenshot size width= ${bitmap2.width}, height=${bitmap2.height}` - ); - - const xScaling = (bitmap1.width - bitmap2.width) / 600; - const yScaling = (bitmap1.height - bitmap2.height) / 400; - const xBorder = Math.round(600 - bitmap2.width / xScaling); - const yBorder = Math.round(400 - bitmap2.height / yScaling); - log.debug( - `======browser======== calculated values xBorder= ${xBorder}, yBorder=${yBorder}, xScaling=${xScaling}, yScaling=${yScaling}` - ); - log.debug( - `======browser======== setting browser size to ${width + xBorder} x ${height + yBorder}` - ); - await this.setWindowSize(width + xBorder, height + yBorder); - - const bitmap3 = await this.getScreenshotAsBitmap(); - // when there is display scaling this won't show the expected size. It will show expected size * scaling factor - log.debug( - `======browser======== final screenshot size width=${bitmap3.width}, height=${bitmap3.height}` - ); - } + const xScaling = (bitmap1.width - bitmap2.width) / 600; + const yScaling = (bitmap1.height - bitmap2.height) / 400; + const xBorder = Math.round(600 - bitmap2.width / xScaling); + const yBorder = Math.round(400 - bitmap2.height / yScaling); + this.log.debug( + `======browser======== calculated values xBorder= ${xBorder}, yBorder=${yBorder}, xScaling=${xScaling}, yScaling=${yScaling}` + ); + this.log.debug( + `======browser======== setting browser size to ${width + xBorder} x ${height + yBorder}` + ); + await this.setWindowSize(width + xBorder, height + yBorder); - /** - * Gets the URL that is loaded in the focused window/frame. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getCurrentUrl - * - * @return {Promise} - */ - public async getCurrentUrl() { - // strip _t=Date query param when url is read - const current = await driver.getCurrentUrl(); - const currentWithoutTime = modifyUrl(current, (parsed) => { - delete (parsed.query as any)._t; + const bitmap3 = await this.getScreenshotAsBitmap(); + // when there is display scaling this won't show the expected size. It will show expected size * scaling factor + this.log.debug( + `======browser======== final screenshot size width=${bitmap3.width}, height=${bitmap3.height}` + ); + } + + /** + * Gets the URL that is loaded in the focused window/frame. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getCurrentUrl + * + * @return {Promise} + */ + public async getCurrentUrl() { + // strip _t=Date query param when url is read + const current = await this.driver.getCurrentUrl(); + const currentWithoutTime = modifyUrl(current, (parsed) => { + delete (parsed.query as any)._t; + return void 0; + }); + return currentWithoutTime; + } + + /** + * Gets the page/document title of the focused window/frame. + * https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#getTitle + */ + public async getTitle() { + return await this.driver.getTitle(); + } + + /** + * Navigates the focused window/frame to a new URL. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#get + * + * @param {string} url + * @param {boolean} insertTimestamp Optional + * @return {Promise} + */ + public async get(url: string, insertTimestamp: boolean = true) { + if (insertTimestamp) { + const urlWithTime = modifyUrl(url, (parsed) => { + (parsed.query as any)._t = Date.now(); return void 0; }); - return currentWithoutTime; - } - - /** - * Gets the page/document title of the focused window/frame. - * https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#getTitle - */ - public async getTitle() { - return await driver.getTitle(); - } - - /** - * Navigates the focused window/frame to a new URL. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/chrome_exports_Driver.html#get - * - * @param {string} url - * @param {boolean} insertTimestamp Optional - * @return {Promise} - */ - public async get(url: string, insertTimestamp: boolean = true) { - if (insertTimestamp) { - const urlWithTime = modifyUrl(url, (parsed) => { - (parsed.query as any)._t = Date.now(); - return void 0; - }); - - return await driver.get(urlWithTime); - } - return await driver.get(url); - } - /** - * Retrieves the cookie with the given name. Returns null if there is no such cookie. The cookie will be returned as - * a JSON object as described by the WebDriver wire protocol. - * https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Options.html - * - * @param {string} cookieName - * @return {Promise} - */ - public async getCookie(cookieName: string) { - return await driver.manage().getCookie(cookieName); - } - - /** - * Pauses the execution in the browser, similar to setting a breakpoint for debugging. - * @return {Promise} - */ - public async pause() { - await driver.executeAsyncScript(`(async () => { debugger; return Promise.resolve(); })()`); - } - - /** - * Moves the remote environment’s mouse cursor to the specified point {x, y} which is - * offset to browser page top left corner. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move - * - * @param {x: number, y: number} point on browser page - * @return {Promise} - */ - public async moveMouseTo(point: { x: number; y: number }): Promise { - await this.getActions().move({ x: 0, y: 0 }).perform(); - await this.getActions().move({ x: point.x, y: point.y, origin: Origin.POINTER }).perform(); - } - - /** - * Does a drag-and-drop action from one point to another - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#dragAndDrop - * - * @return {Promise} - */ - public async dragAndDrop( - from: { - location: WebElementWrapper | { x?: number; y?: number }; - offset?: { x?: number; y?: number }; - }, - to: { - location: WebElementWrapper | { x?: number; y?: number }; - offset?: { x?: number; y?: number }; + return await this.driver.get(urlWithTime); + } + return await this.driver.get(url); + } + + /** + * Retrieves the cookie with the given name. Returns null if there is no such cookie. The cookie will be returned as + * a JSON object as described by the WebDriver wire protocol. + * https://www.selenium.dev/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Options.html + * + * @param {string} cookieName + * @return {Promise} + */ + public async getCookie(cookieName: string) { + return await this.driver.manage().getCookie(cookieName); + } + + /** + * Pauses the execution in the browser, similar to setting a breakpoint for debugging. + * @return {Promise} + */ + public async pause() { + await this.driver.executeAsyncScript(`(async () => { debugger; return Promise.resolve(); })()`); + } + + /** + * Moves the remote environment’s mouse cursor to the specified point {x, y} which is + * offset to browser page top left corner. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#move + * + * @param {x: number, y: number} point on browser page + * @return {Promise} + */ + public async moveMouseTo(point: { x: number; y: number }): Promise { + await this.getActions().move({ x: 0, y: 0 }).perform(); + await this.getActions().move({ x: point.x, y: point.y, origin: Origin.POINTER }).perform(); + } + + /** + * Does a drag-and-drop action from one point to another + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#dragAndDrop + * + * @return {Promise} + */ + public async dragAndDrop( + from: { + location: WebElementWrapper | { x?: number; y?: number }; + offset?: { x?: number; y?: number }; + }, + to: { + location: WebElementWrapper | { x?: number; y?: number }; + offset?: { x?: number; y?: number }; + } + ) { + // The offset should be specified in pixels relative to the center of the element's bounding box + const getW3CPoint = (data: any) => { + if (!data.offset) { + data.offset = {}; } - ) { - // The offset should be specified in pixels relative to the center of the element's bounding box - const getW3CPoint = (data: any) => { - if (!data.offset) { - data.offset = {}; - } - return data.location instanceof WebElementWrapper - ? { - x: data.offset.x || 0, - y: data.offset.y || 0, - origin: data.location._webElement, - } - : { x: data.location.x, y: data.location.y, origin: Origin.POINTER }; - }; - - const startPoint = getW3CPoint(from); - const endPoint = getW3CPoint(to); - await this.getActions().move({ x: 0, y: 0 }).perform(); - return await this.getActions().move(startPoint).press().move(endPoint).release().perform(); - } - - /** - * Performs drag and drop for html5 native drag and drop implementation - * There's a bug in Chromedriver for html5 dnd that doesn't allow to use the method `dragAndDrop` defined above - * https://github.com/SeleniumHQ/selenium/issues/6235 - * This implementation simulates user's action by calling the drag and drop specific events directly. - * - * @param {string} from html selector - * @param {string} to html selector - * @return {Promise} - */ - public async html5DragAndDrop(from: string, to: string) { - await this.execute( - ` - function createEvent(typeOfEvent) { - const event = document.createEvent("CustomEvent"); - event.initCustomEvent(typeOfEvent, true, true, null); - event.dataTransfer = { - data: {}, - setData: function (key, value) { - this.data[key] = value; - }, - getData: function (key) { - return this.data[key]; - } - }; - return event; + return data.location instanceof WebElementWrapper + ? { + x: data.offset.x || 0, + y: data.offset.y || 0, + origin: data.location._webElement, } - function dispatchEvent(element, event, transferData) { - if (transferData !== undefined) { - event.dataTransfer = transferData; - } - if (element.dispatchEvent) { - element.dispatchEvent(event); - } else if (element.fireEvent) { - element.fireEvent("on" + event.type, event); + : { x: data.location.x, y: data.location.y, origin: Origin.POINTER }; + }; + + const startPoint = getW3CPoint(from); + const endPoint = getW3CPoint(to); + await this.getActions().move({ x: 0, y: 0 }).perform(); + return await this.getActions().move(startPoint).press().move(endPoint).release().perform(); + } + + /** + * Performs drag and drop for html5 native drag and drop implementation + * There's a bug in Chromedriver for html5 dnd that doesn't allow to use the method `dragAndDrop` defined above + * https://github.com/SeleniumHQ/selenium/issues/6235 + * This implementation simulates user's action by calling the drag and drop specific events directly. + * + * @param {string} from html selector + * @param {string} to html selector + * @return {Promise} + */ + public async html5DragAndDrop(from: string, to: string) { + await this.execute( + ` + function createEvent(typeOfEvent) { + const event = document.createEvent("CustomEvent"); + event.initCustomEvent(typeOfEvent, true, true, null); + event.dataTransfer = { + data: {}, + setData: function (key, value) { + this.data[key] = value; + }, + getData: function (key) { + return this.data[key]; } + }; + return event; + } + function dispatchEvent(element, event, transferData) { + if (transferData !== undefined) { + event.dataTransfer = transferData; } - - const origin = document.querySelector(arguments[0]); - - const dragStartEvent = createEvent('dragstart'); - dispatchEvent(origin, dragStartEvent); - - setTimeout(() => { - const dropEvent = createEvent('drop'); - const target = document.querySelector(arguments[1]); - dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer); - const dragEndEvent = createEvent('dragend'); - dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer); - }, 100); - `, - from, - to - ); - // wait for 150ms to make sure the script has run - await delay(150); - } - - /** - * Reloads the current browser window/frame. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#refresh - * - * @return {Promise} - */ - public async refresh() { - await driver.navigate().refresh(); - } - - /** - * Navigates the focused window/frame back one page using the browser’s navigation history. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#back - * - * @return {Promise} - */ - public async goBack() { - await driver.navigate().back(); - } - - /** - * Moves forwards in the browser history. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#forward - * - * @return {Promise} - */ - public async goForward() { - await driver.navigate().forward(); - } - - /** - * Navigates to a URL via the browser history. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#to - * - * @return {Promise} - */ - public async navigateTo(url: string) { - await driver.navigate().to(url); - } - - /** - * Sends a sequance of keyboard keys. For each key, this will record a pair of keyDown and keyUp actions - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#sendKeys - * - * @param {string|string[]} keys - * @return {Promise} - */ - public async pressKeys(keys: string | string[]): Promise; - public async pressKeys(...args: string[]): Promise; - public async pressKeys(...args: string[]): Promise { - const chord = this.keys.chord(...args); - await this.getActions().sendKeys(chord).perform(); - } - - /** - * Moves the remote environment’s mouse cursor to the specified point {x, y} which is - * offset to browser page top left corner. - * Then adds an action for left-click (down/up) with the mouse. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#click - * - * @param {x: number, y: number} point on browser page - * @return {Promise} - */ - public async clickMouseButton(point: { x: number; y: number }) { - await this.getActions().move({ x: 0, y: 0 }).perform(); - await this.getActions() - .move({ x: point.x, y: point.y, origin: Origin.POINTER }) - .click() - .perform(); - } - - /** - * Gets the HTML loaded in the focused window/frame. This markup is serialised by the remote - * environment so may not exactly match the HTML provided by the Web server. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getPageSource - * - * @return {Promise} - */ - public async getPageSource() { - return await driver.getPageSource(); - } - - /** - * Gets a screenshot of the focused window and returns it as a base-64 encoded PNG - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#takeScreenshot - * - * @return {Promise} - */ - public async takeScreenshot() { - return await driver.takeScreenshot(); - } - - /** - * Inserts action for performing a double left-click with the mouse. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#doubleClick - * @param {WebElementWrapper} element - * @return {Promise} - */ - public async doubleClick() { - await this.getActions().doubleClick().perform(); - } - - /** - * Changes the focus of all future commands to another window. Windows may be specified - * by their window.name attributeor by its handle (as returned by WebDriver#getWindowHandles). - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_TargetLocator.html - * - * @param {string} handle - * @return {Promise} - */ - public async switchToWindow(nameOrHandle: string) { - await driver.switchTo().window(nameOrHandle); - } - - /** - * Gets a list of identifiers for all currently open windows. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getAllWindowHandles - * - * @return {Promise} - */ - public async getAllWindowHandles() { - return await driver.getAllWindowHandles(); - } - - /** - * Switches driver to specific browser tab by index - * - * @param {string} tabIndex - * @return {Promise} - */ - public async switchTab(tabIndex: number) { - const tabs = await driver.getAllWindowHandles(); - if (tabs.length <= tabIndex) { - throw new Error(`Out of existing tabs bounds`); - } - await driver.switchTo().window(tabs[tabIndex]); - } - - /** - * Sets a value in local storage for the focused window/frame. - * - * @param {string} key - * @param {string} value - * @return {Promise} - */ - public async setLocalStorageItem(key: string, value: string): Promise { - await driver.executeScript( - 'return window.localStorage.setItem(arguments[0], arguments[1]);', - key, - value - ); - } - - /** - * Removes a value in local storage for the focused window/frame. - * - * @param {string} key - * @return {Promise} - */ - public async removeLocalStorageItem(key: string): Promise { - await driver.executeScript('return window.localStorage.removeItem(arguments[0]);', key); - } - - /** - * Clears session storage for the focused window/frame. - * - * @return {Promise} - */ - public async clearSessionStorage(): Promise { - await driver.executeScript('return window.sessionStorage.clear();'); - } - - /** - * Closes the currently focused window. In most environments, after the window has been - * closed, it is necessary to explicitly switch to whatever window is now focused. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#close - * - * @return {Promise} - */ - public async closeCurrentWindow() { - await driver.close(); - } - - /** - * Executes JavaScript code within the focused window/frame. The code should return a value synchronously. - * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#executeScript - * - * @param {string|function} fn - * @param {...any[]} args - */ - public async execute( - fn: string | ((...args: A) => R), - ...args: A - ): Promise { - return await driver.executeScript( - fn, - ...cloneDeepWith(args, (arg) => { - if (arg instanceof WebElementWrapper) { - return arg._webElement; - } - }) - ); - } - - public async executeAsync(fn: (cb: (value?: T) => void) => void): Promise; - public async executeAsync( - fn: (a1: A1, cb: (value?: T) => void) => void, - a1: A1 - ): Promise; - public async executeAsync( - fn: (a1: A1, a2: A2, cb: (value?: T) => void) => void, - a1: A1, - a2: A2 - ): Promise; - public async executeAsync( - fn: (a1: A1, a2: A2, a3: A3, cb: (value?: T) => void) => void, - a1: A1, - a2: A2, - a3: A3 - ): Promise; - public async executeAsync( - fn: (...args: any[]) => void, - ...args: any[] - ): Promise { - return await driver.executeAsyncScript( - fn, - ...cloneDeepWith(args, (arg) => { - if (arg instanceof WebElementWrapper) { - return arg._webElement; + if (element.dispatchEvent) { + element.dispatchEvent(event); + } else if (element.fireEvent) { + element.fireEvent("on" + event.type, event); } - }) - ); - } - - public async getScrollTop() { - const scrollSize = await driver.executeScript('return document.body.scrollTop'); - return parseInt(scrollSize, 10); - } - - public async getScrollLeft() { - const scrollSize = await driver.executeScript('return document.body.scrollLeft'); - return parseInt(scrollSize, 10); - } - - public async scrollTop() { - await driver.executeScript('document.documentElement.scrollTop = 0'); - } + } - // return promise with REAL scroll position - public async setScrollTop(scrollSize: number | string) { - await driver.executeScript('document.body.scrollTop = ' + scrollSize); - return this.getScrollTop(); - } + const origin = document.querySelector(arguments[0]); + + const dragStartEvent = createEvent('dragstart'); + dispatchEvent(origin, dragStartEvent); + + setTimeout(() => { + const dropEvent = createEvent('drop'); + const target = document.querySelector(arguments[1]); + dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer); + const dragEndEvent = createEvent('dragend'); + dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer); + }, 100); + `, + from, + to + ); + // wait for 150ms to make sure the script has run + await delay(150); + } + + /** + * Reloads the current browser window/frame. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#refresh + * + * @return {Promise} + */ + public async refresh() { + await this.driver.navigate().refresh(); + } + + /** + * Navigates the focused window/frame back one page using the browser’s navigation history. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#back + * + * @return {Promise} + */ + public async goBack() { + await this.driver.navigate().back(); + } + + /** + * Moves forwards in the browser history. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#forward + * + * @return {Promise} + */ + public async goForward() { + await this.driver.navigate().forward(); + } + + /** + * Navigates to a URL via the browser history. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#to + * + * @return {Promise} + */ + public async navigateTo(url: string) { + await this.driver.navigate().to(url); + } + + /** + * Sends a sequance of keyboard keys. For each key, this will record a pair of keyDown and keyUp actions + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#sendKeys + * + * @param {string|string[]} keys + * @return {Promise} + */ + public async pressKeys(keys: string | string[]): Promise; + public async pressKeys(...args: string[]): Promise; + public async pressKeys(...args: string[]): Promise { + const chord = this.keys.chord(...args); + await this.getActions().sendKeys(chord).perform(); + } + + /** + * Moves the remote environment’s mouse cursor to the specified point {x, y} which is + * offset to browser page top left corner. + * Then adds an action for left-click (down/up) with the mouse. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#click + * + * @param {x: number, y: number} point on browser page + * @return {Promise} + */ + public async clickMouseButton(point: { x: number; y: number }) { + await this.getActions().move({ x: 0, y: 0 }).perform(); + await this.getActions() + .move({ x: point.x, y: point.y, origin: Origin.POINTER }) + .click() + .perform(); + } + + /** + * Gets the HTML loaded in the focused window/frame. This markup is serialised by the remote + * environment so may not exactly match the HTML provided by the Web server. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getPageSource + * + * @return {Promise} + */ + public async getPageSource() { + return await this.driver.getPageSource(); + } + + /** + * Gets a screenshot of the focused window and returns it as a base-64 encoded PNG + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#takeScreenshot + * + * @return {Promise} + */ + public async takeScreenshot() { + return await this.driver.takeScreenshot(); + } + + /** + * Inserts action for performing a double left-click with the mouse. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#doubleClick + * @param {WebElementWrapper} element + * @return {Promise} + */ + public async doubleClick() { + await this.getActions().doubleClick().perform(); + } + + /** + * Changes the focus of all future commands to another window. Windows may be specified + * by their window.name attributeor by its handle (as returned by WebDriver#getWindowHandles). + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_TargetLocator.html + * + * @param {string} handle + * @return {Promise} + */ + public async switchToWindow(nameOrHandle: string) { + await this.driver.switchTo().window(nameOrHandle); + } + + /** + * Gets a list of identifiers for all currently open windows. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#getAllWindowHandles + * + * @return {Promise} + */ + public async getAllWindowHandles() { + return await this.driver.getAllWindowHandles(); + } + + /** + * Switches driver to specific browser tab by index + * + * @param {string} tabIndex + * @return {Promise} + */ + public async switchTab(tabIndex: number) { + const tabs = await this.driver.getAllWindowHandles(); + if (tabs.length <= tabIndex) { + throw new Error(`Out of existing tabs bounds`); + } + await this.driver.switchTo().window(tabs[tabIndex]); + } + + /** + * Sets a value in local storage for the focused window/frame. + * + * @param {string} key + * @param {string} value + * @return {Promise} + */ + public async setLocalStorageItem(key: string, value: string): Promise { + await this.driver.executeScript( + 'return window.localStorage.setItem(arguments[0], arguments[1]);', + key, + value + ); + } + + /** + * Removes a value in local storage for the focused window/frame. + * + * @param {string} key + * @return {Promise} + */ + public async removeLocalStorageItem(key: string): Promise { + await this.driver.executeScript('return window.localStorage.removeItem(arguments[0]);', key); + } + + /** + * Clears session storage for the focused window/frame. + * + * @return {Promise} + */ + public async clearSessionStorage(): Promise { + await this.driver.executeScript('return window.sessionStorage.clear();'); + } + + /** + * Closes the currently focused window. In most environments, after the window has been + * closed, it is necessary to explicitly switch to whatever window is now focused. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#close + * + * @return {Promise} + */ + public async closeCurrentWindow() { + await this.driver.close(); + } + + /** + * Executes JavaScript code within the focused window/frame. The code should return a value synchronously. + * https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#executeScript + * + * @param {string|function} fn + * @param {...any[]} args + */ + public async execute( + fn: string | ((...args: A) => R), + ...args: A + ): Promise { + return await this.driver.executeScript( + fn, + ...cloneDeepWith(args, (arg) => { + if (arg instanceof WebElementWrapper) { + return arg._webElement; + } + }) + ); + } + + public async executeAsync(fn: (cb: (value?: T) => void) => void): Promise; + public async executeAsync( + fn: (a1: A1, cb: (value?: T) => void) => void, + a1: A1 + ): Promise; + public async executeAsync( + fn: (a1: A1, a2: A2, cb: (value?: T) => void) => void, + a1: A1, + a2: A2 + ): Promise; + public async executeAsync( + fn: (a1: A1, a2: A2, a3: A3, cb: (value?: T) => void) => void, + a1: A1, + a2: A2, + a3: A3 + ): Promise; + public async executeAsync(fn: (...args: any[]) => void, ...args: any[]): Promise { + return await this.driver.executeAsyncScript( + fn, + ...cloneDeepWith(args, (arg) => { + if (arg instanceof WebElementWrapper) { + return arg._webElement; + } + }) + ); + } + + public async getScrollTop() { + const scrollSize = await this.driver.executeScript('return document.body.scrollTop'); + return parseInt(scrollSize, 10); + } + + public async getScrollLeft() { + const scrollSize = await this.driver.executeScript('return document.body.scrollLeft'); + return parseInt(scrollSize, 10); + } + + public async scrollTop() { + await this.driver.executeScript('document.documentElement.scrollTop = 0'); + } + + // return promise with REAL scroll position + public async setScrollTop(scrollSize: number | string) { + await this.driver.executeScript('document.body.scrollTop = ' + scrollSize); + return this.getScrollTop(); + } + + public async setScrollToById(elementId: string, xCoord: number, yCoord: number) { + await this.driver.executeScript( + `document.getElementById("${elementId}").scrollTo(${xCoord},${yCoord})` + ); + } - public async setScrollToById(elementId: string, xCoord: number, yCoord: number) { - await driver.executeScript( - `document.getElementById("${elementId}").scrollTo(${xCoord},${yCoord})` - ); - } + public async setScrollLeft(scrollSize: number | string) { + await this.driver.executeScript('document.body.scrollLeft = ' + scrollSize); + return this.getScrollLeft(); + } - public async setScrollLeft(scrollSize: number | string) { - await driver.executeScript('document.body.scrollLeft = ' + scrollSize); - return this.getScrollLeft(); - } + public async switchToFrame(idOrElement: number | WebElementWrapper) { + const _id = idOrElement instanceof WebElementWrapper ? idOrElement._webElement : idOrElement; + await this.driver.switchTo().frame(_id); + } - public async switchToFrame(idOrElement: number | WebElementWrapper) { - const _id = idOrElement instanceof WebElementWrapper ? idOrElement._webElement : idOrElement; - await driver.switchTo().frame(_id); - } + public async checkBrowserPermission(permission: string): Promise { + const result: any = await this.driver.executeAsyncScript( + `navigator.permissions.query({name:'${permission}'}).then(arguments[0])` + ); - public async checkBrowserPermission(permission: string): Promise { - const result: any = await driver.executeAsyncScript( - `navigator.permissions.query({name:'${permission}'}).then(arguments[0])` - ); + return Boolean(result?.state === 'granted'); + } - return Boolean(result?.state === 'granted'); - } + public getClipboardValue(): Promise { + return this.driver.executeAsyncScript('navigator.clipboard.readText().then(arguments[0])'); + } +} - public getClipboardValue(): Promise { - return driver.executeAsyncScript('navigator.clipboard.readText().then(arguments[0])'); - } - })(); +export async function BrowserProvider(ctx: FtrProviderContext) { + const { driver, browserType } = await ctx.getService('__webdriver__').init(); + return new BrowserService(ctx, browserType, driver); } diff --git a/test/functional/services/common/find.ts b/test/functional/services/common/find.ts index 0cd4c14683f6e9..8d037e2df2109d 100644 --- a/test/functional/services/common/find.ts +++ b/test/functional/services/common/find.ts @@ -7,511 +7,474 @@ */ import { WebDriver, WebElement, By, until } from 'selenium-webdriver'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { WebElementWrapper } from '../lib/web_element_wrapper'; -export async function FindProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const config = getService('config'); - const { driver, browserType } = await getService('__webdriver__').init(); - const retry = getService('retry'); +import { Browsers } from '../remote/browsers'; +import { FtrService, FtrProviderContext } from '../../ftr_provider_context'; +import { WebElementWrapper } from '../lib/web_element_wrapper'; - const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists'); - const POLLING_TIME = 500; - const defaultFindTimeout = config.get('timeouts.find'); - const fixedHeaderHeight = config.get('layout.fixedHeaderHeight'); +export class FindService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly config = this.ctx.getService('config'); + private readonly retry = this.ctx.getService('retry'); - const wrap = (webElement: WebElement | WebElementWrapper, locator: By | null = null) => - WebElementWrapper.create( - webElement, - locator, - driver, - defaultFindTimeout, - fixedHeaderHeight, - log, - browserType - ); + private readonly WAIT_FOR_EXISTS_TIME = this.config.get('timeouts.waitForExists'); + private readonly POLLING_TIME = 500; + private readonly defaultFindTimeout = this.config.get('timeouts.find'); + private readonly fixedHeaderHeight = this.config.get('layout.fixedHeaderHeight'); - const wrapAll = (webElements: Array) => - webElements.map((e) => wrap(e)); + public currentWait = this.defaultFindTimeout; - const findAndWrap = async (locator: By, timeout: number): Promise => { - const webElement = await driver.wait(until.elementLocated(locator), timeout); - return wrap(webElement, locator); - }; + constructor( + ctx: FtrProviderContext, + private readonly browserType: Browsers, + private readonly driver: WebDriver + ) { + super(ctx); + } - class Find { - public currentWait = defaultFindTimeout; + public async byName( + selector: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.byName('${selector}') with timeout=${timeout}`); + return await this.findAndWrap(By.name(selector), timeout); + } - public async byName( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.byName('${selector}') with timeout=${timeout}`); - return await findAndWrap(By.name(selector), timeout); - } + public async byCssSelector( + selector: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.findByCssSelector('${selector}') with timeout=${timeout}`); + return this.findAndWrap(By.css(selector), timeout); + } - public async byCssSelector( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.findByCssSelector('${selector}') with timeout=${timeout}`); - return findAndWrap(By.css(selector), timeout); - } + public async byXPath( + selector: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.byXPath('${selector}') with timeout=${timeout}`); + return this.findAndWrap(By.xpath(selector), timeout); + } - public async byXPath( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.byXPath('${selector}') with timeout=${timeout}`); - return findAndWrap(By.xpath(selector), timeout); - } + public async byClassName( + selector: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.findByClassName('${selector}') with timeout=${timeout}`); + return this.findAndWrap(By.className(selector), timeout); + } - public async byClassName( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.findByClassName('${selector}') with timeout=${timeout}`); - return findAndWrap(By.className(selector), timeout); - } + public async activeElement(): Promise { + return this.wrap(await this.driver.switchTo().activeElement()); + } - public async activeElement(): Promise { - return wrap(await driver.switchTo().activeElement()); - } + public async setValue(selector: string, text: string, topOffset?: number): Promise { + this.log.debug(`Find.setValue('${selector}', '${text}')`); + return await this.retry.try(async () => { + const element = await this.byCssSelector(selector); + await element.click(topOffset); + + // in case the input element is actually a child of the testSubject, we + // call clearValue() and type() on the element that is focused after + // clicking on the testSubject + const input = await this.activeElement(); + if (input) { + await input.clearValue(); + await input.type(text); + } else { + await element.clearValue(); + await element.type(text); + } + }); + } - public async setValue(selector: string, text: string, topOffset?: number): Promise { - log.debug(`Find.setValue('${selector}', '${text}')`); - return await retry.try(async () => { - const element = await this.byCssSelector(selector); - await element.click(topOffset); + public async selectValue(selector: string, value: string): Promise { + this.log.debug(`Find.selectValue('${selector}', option[value="${value}"]')`); + const combobox = await this.byCssSelector(selector); + const $ = await combobox.parseDomContent(); + const text = $(`option[value="${value}"]`).text(); + await combobox.type(text); + } - // in case the input element is actually a child of the testSubject, we - // call clearValue() and type() on the element that is focused after - // clicking on the testSubject - const input = await this.activeElement(); - if (input) { - await input.clearValue(); - await input.type(text); - } else { - await element.clearValue(); - await element.type(text); + public async filterElementIsDisplayed(elements: WebElementWrapper[]) { + if (elements.length === 0) { + return []; + } else { + const displayed = []; + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < elements.length; i++) { + const isDisplayed = await elements[i].isDisplayed(); + if (isDisplayed) { + displayed.push(elements[i]); } - }); + } + return displayed; } + } - public async selectValue(selector: string, value: string): Promise { - log.debug(`Find.selectValue('${selector}', option[value="${value}"]')`); - const combobox = await this.byCssSelector(selector); - const $ = await combobox.parseDomContent(); - const text = $(`option[value="${value}"]`).text(); - await combobox.type(text); - } + public async allByCssSelector( + selector: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.allByCssSelector('${selector}') with timeout=${timeout}`); + await this._withTimeout(timeout); + const elements = await this.driver.findElements(By.css(selector)); + await this._withTimeout(this.defaultFindTimeout); + return this.wrapAll(elements); + } - public async filterElementIsDisplayed(elements: WebElementWrapper[]) { - if (elements.length === 0) { - return []; - } else { - const displayed = []; - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < elements.length; i++) { - const isDisplayed = await elements[i].isDisplayed(); - if (isDisplayed) { - displayed.push(elements[i]); - } - } - return displayed; - } - } + public async descendantExistsByCssSelector( + selector: string, + parentElement: WebElementWrapper, + timeout: number = this.WAIT_FOR_EXISTS_TIME + ): Promise { + this.log.debug(`Find.descendantExistsByCssSelector('${selector}') with timeout=${timeout}`); + const els = await parentElement._webElement.findElements(By.css(selector)); + return await this.exists(async () => this.wrapAll(els), timeout); + } - public async allByCustom( - findAllFunction: (drive: WebDriver) => WebElementWrapper[], - timeout = defaultFindTimeout - ): Promise { - await this._withTimeout(timeout); - return await retry.try(async () => { - let elements = await findAllFunction(driver); - if (!elements) { - elements = []; - } - // Force isStale checks for all the retrieved elements. - await Promise.all(elements.map(async (element) => await element.isEnabled())); - await this._withTimeout(defaultFindTimeout); - return elements; - }); + public async descendantDisplayedByCssSelector( + selector: string, + parentElement: WebElementWrapper + ): Promise { + this.log.debug(`Find.descendantDisplayedByCssSelector('${selector}')`); + const element = await parentElement._webElement.findElement(By.css(selector)); + const descendant = this.wrap(element, By.css(selector)); + const isDisplayed = await descendant.isDisplayed(); + if (isDisplayed) { + return descendant; + } else { + throw new Error(`Element "${selector}" is not displayed`); } + } - public async allByLinkText( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.allByLinkText('${selector}') with timeout=${timeout}`); - await this._withTimeout(timeout); - const elements = await driver.findElements(By.linkText(selector)); - await this._withTimeout(defaultFindTimeout); - return wrapAll(elements); - } + public async allDescendantDisplayedByCssSelector( + selector: string, + parentElement: WebElementWrapper + ): Promise { + this.log.debug(`Find.allDescendantDisplayedByCssSelector('${selector}')`); + const allElements = await this.wrapAll( + await parentElement._webElement.findElements(By.css(selector)) + ); + return await this.filterElementIsDisplayed(allElements); + } - public async allByButtonText( - buttonText: string, - element: WebDriver | WebElement | WebElementWrapper = driver, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.byButtonText('${buttonText}') with timeout=${timeout}`); - return await retry.tryForTime(timeout, async () => { - // tslint:disable-next-line:variable-name - const _element = element instanceof WebElementWrapper ? element._webElement : element; - await this._withTimeout(0); - const allButtons = wrapAll(await _element.findElements(By.tagName('button'))); - await this._withTimeout(defaultFindTimeout); - const buttonTexts = await Promise.all( - allButtons.map(async (el) => { - return el.getVisibleText(); - }) - ); - return buttonTexts.filter((text) => text.trim() === buttonText.trim()); - }); - } + public async allDescendantDisplayedByTagName( + tagName: string, + parentElement: WebElementWrapper + ): Promise { + this.log.debug(`Find.allDescendantDisplayedByTagName('${tagName}')`); + const allElements = await this.wrapAll( + await parentElement._webElement.findElements(By.tagName(tagName)) + ); + return await this.filterElementIsDisplayed(allElements); + } - public async allByCssSelector( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.allByCssSelector('${selector}') with timeout=${timeout}`); - await this._withTimeout(timeout); - const elements = await driver.findElements(By.css(selector)); - await this._withTimeout(defaultFindTimeout); - return wrapAll(elements); - } + public async displayedByLinkText( + linkText: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.displayedByLinkText('${linkText}') with timeout=${timeout}`); + const element = await this.byLinkText(linkText, timeout); + this.log.debug(`Wait for element become visible: ${linkText} with timeout=${timeout}`); + await this.driver.wait(until.elementIsVisible(element._webElement), timeout); + return this.wrap(element, By.linkText(linkText)); + } - public async descendantExistsByCssSelector( - selector: string, - parentElement: WebElementWrapper, - timeout: number = WAIT_FOR_EXISTS_TIME - ): Promise { - log.debug(`Find.descendantExistsByCssSelector('${selector}') with timeout=${timeout}`); - const els = await parentElement._webElement.findElements(By.css(selector)); - return await this.exists(async () => wrapAll(els), timeout); - } + public async displayedByCssSelector( + selector: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.displayedByCssSelector(${selector})`); + const element = await this.byCssSelector(selector, timeout); + this.log.debug(`Wait for element become visible: ${selector} with timeout=${timeout}`); + await this.driver.wait(until.elementIsVisible(element._webElement), timeout); + return this.wrap(element, By.css(selector)); + } + + public async byLinkText( + selector: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.byLinkText('${selector}') with timeout=${timeout}`); + return this.findAndWrap(By.linkText(selector), timeout); + } - public async descendantDisplayedByCssSelector( - selector: string, - parentElement: WebElementWrapper - ): Promise { - log.debug(`Find.descendantDisplayedByCssSelector('${selector}')`); - const element = await parentElement._webElement.findElement(By.css(selector)); - const descendant = wrap(element, By.css(selector)); - const isDisplayed = await descendant.isDisplayed(); - if (isDisplayed) { - return descendant; + public async byPartialLinkText( + partialLinkText: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.byPartialLinkText('${partialLinkText}') with timeout=${timeout}`); + return this.findAndWrap(By.partialLinkText(partialLinkText), timeout); + } + + public async exists( + findFunction: ( + el: WebDriver + ) => + | WebElementWrapper + | WebElementWrapper[] + | Promise + | Promise, + timeout: number = this.WAIT_FOR_EXISTS_TIME + ): Promise { + await this._withTimeout(timeout); + try { + const found = await findFunction(this.driver); + await this._withTimeout(this.defaultFindTimeout); + if (Array.isArray(found)) { + return found.length > 0; } else { - throw new Error(`Element "${selector}" is not displayed`); + return found instanceof WebElementWrapper; } + } catch (err) { + await this._withTimeout(this.defaultFindTimeout); + return false; } + } - public async allDescendantDisplayedByCssSelector( - selector: string, - parentElement: WebElementWrapper - ): Promise { - log.debug(`Find.allDescendantDisplayedByCssSelector('${selector}')`); - const allElements = await wrapAll( - await parentElement._webElement.findElements(By.css(selector)) - ); - return await this.filterElementIsDisplayed(allElements); - } - - public async allDescendantDisplayedByTagName( - tagName: string, - parentElement: WebElementWrapper - ): Promise { - log.debug(`Find.allDescendantDisplayedByTagName('${tagName}')`); - const allElements = await wrapAll( - await parentElement._webElement.findElements(By.tagName(tagName)) - ); - return await this.filterElementIsDisplayed(allElements); - } + public async existsByLinkText( + linkText: string, + timeout: number = this.WAIT_FOR_EXISTS_TIME + ): Promise { + this.log.debug(`Find.existsByLinkText('${linkText}') with timeout=${timeout}`); + return await this.exists( + async (drive) => this.wrapAll(await drive.findElements(By.linkText(linkText))), + timeout + ); + } - public async displayedByLinkText( - linkText: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.displayedByLinkText('${linkText}') with timeout=${timeout}`); - const element = await this.byLinkText(linkText, timeout); - log.debug(`Wait for element become visible: ${linkText} with timeout=${timeout}`); - await driver.wait(until.elementIsVisible(element._webElement), timeout); - return wrap(element, By.linkText(linkText)); + public async existsByDisplayedByCssSelector( + selector: string, + timeout: number = this.WAIT_FOR_EXISTS_TIME + ): Promise { + this.log.debug(`Find.existsByDisplayedByCssSelector('${selector}') with timeout=${timeout}`); + try { + await this.retry.tryForTime(timeout, async () => { + // make sure that the find timeout is not longer than the retry timeout + await this._withTimeout(Math.min(timeout, this.WAIT_FOR_EXISTS_TIME)); + const elements = await this.driver.findElements(By.css(selector)); + await this._withTimeout(this.defaultFindTimeout); + const displayed = await this.filterElementIsDisplayed(this.wrapAll(elements)); + if (displayed.length === 0) { + throw new Error(`${selector} is not displayed`); + } + }); + } catch (err) { + await this._withTimeout(this.defaultFindTimeout); + return false; } + return true; + } - public async displayedByCssSelector( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.displayedByCssSelector(${selector})`); - const element = await this.byCssSelector(selector, timeout); - log.debug(`Wait for element become visible: ${selector} with timeout=${timeout}`); - await driver.wait(until.elementIsVisible(element._webElement), timeout); - return wrap(element, By.css(selector)); - } + public async existsByCssSelector( + selector: string, + timeout: number = this.WAIT_FOR_EXISTS_TIME + ): Promise { + this.log.debug(`Find.existsByCssSelector('${selector}') with timeout=${timeout}`); + return await this.exists(async (drive) => { + return this.wrapAll(await drive.findElements(By.css(selector))); + }, timeout); + } - public async byLinkText( - selector: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.byLinkText('${selector}') with timeout=${timeout}`); - return findAndWrap(By.linkText(selector), timeout); - } + public async clickByCssSelectorWhenNotDisabled( + selector: string, + { timeout } = { timeout: this.defaultFindTimeout } + ): Promise { + this.log.debug(`Find.clickByCssSelectorWhenNotDisabled('${selector}') with timeout=${timeout}`); + + // Don't wrap this code in a retry, or stale element checks may get caught here and the element + // will never be re-grabbed. Let errors bubble, but continue checking for disabled property until + // it's gone. + const element = await this.byCssSelector(selector, timeout); + await element.moveMouseTo(); + await this.driver.wait(until.elementIsEnabled(element._webElement), timeout); + await element.click(); + } - public async byPartialLinkText( - partialLinkText: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.byPartialLinkText('${partialLinkText}') with timeout=${timeout}`); - return findAndWrap(By.partialLinkText(partialLinkText), timeout); - } + public async clickByPartialLinkText( + linkText: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.clickByPartialLinkText('${linkText}') with timeout=${timeout}`); + await this.retry.try(async () => { + const element = await this.byPartialLinkText(linkText, timeout); + await element.moveMouseTo(); + await element.click(); + }); + } - public async exists( - findFunction: ( - el: WebDriver - ) => - | WebElementWrapper - | WebElementWrapper[] - | Promise - | Promise, - timeout: number = WAIT_FOR_EXISTS_TIME - ): Promise { - await this._withTimeout(timeout); - try { - const found = await findFunction(driver); - await this._withTimeout(defaultFindTimeout); - if (Array.isArray(found)) { - return found.length > 0; - } else { - return found instanceof WebElementWrapper; - } - } catch (err) { - await this._withTimeout(defaultFindTimeout); - return false; - } - } + public async clickByLinkText( + linkText: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.clickByLinkText('${linkText}') with timeout=${timeout}`); + await this.retry.try(async () => { + const element = await this.byLinkText(linkText, timeout); + await element.moveMouseTo(); + await element.click(); + }); + } - public async existsByLinkText( - linkText: string, - timeout: number = WAIT_FOR_EXISTS_TIME - ): Promise { - log.debug(`Find.existsByLinkText('${linkText}') with timeout=${timeout}`); - return await this.exists( - async (drive) => wrapAll(await drive.findElements(By.linkText(linkText))), - timeout + public async byButtonText( + buttonText: string, + element: WebDriver | WebElement | WebElementWrapper = this.driver, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.byButtonText('${buttonText}') with timeout=${timeout}`); + return await this.retry.tryForTime(timeout, async () => { + // tslint:disable-next-line:variable-name + const _element = element instanceof WebElementWrapper ? element._webElement : element; + const allButtons = this.wrapAll(await _element.findElements(By.tagName('button'))); + const buttonTexts = await Promise.all( + allButtons.map(async (el) => { + return el.getVisibleText(); + }) ); - } - - public async existsByDisplayedByCssSelector( - selector: string, - timeout: number = WAIT_FOR_EXISTS_TIME - ): Promise { - log.debug(`Find.existsByDisplayedByCssSelector('${selector}') with timeout=${timeout}`); - try { - await retry.tryForTime(timeout, async () => { - // make sure that the find timeout is not longer than the retry timeout - await this._withTimeout(Math.min(timeout, WAIT_FOR_EXISTS_TIME)); - const elements = await driver.findElements(By.css(selector)); - await this._withTimeout(defaultFindTimeout); - const displayed = await this.filterElementIsDisplayed(wrapAll(elements)); - if (displayed.length === 0) { - throw new Error(`${selector} is not displayed`); - } - }); - } catch (err) { - await this._withTimeout(defaultFindTimeout); - return false; + const index = buttonTexts.findIndex((text) => text.trim() === buttonText.trim()); + if (index === -1) { + throw new Error('Button not found'); } - return true; - } - - public async existsByCssSelector( - selector: string, - timeout: number = WAIT_FOR_EXISTS_TIME - ): Promise { - log.debug(`Find.existsByCssSelector('${selector}') with timeout=${timeout}`); - return await this.exists(async (drive) => { - return wrapAll(await drive.findElements(By.css(selector))); - }, timeout); - } + return allButtons[index]; + }); + } - public async clickByCssSelectorWhenNotDisabled( - selector: string, - { timeout } = { timeout: defaultFindTimeout } - ): Promise { - log.debug(`Find.clickByCssSelectorWhenNotDisabled('${selector}') with timeout=${timeout}`); + public async clickByButtonText( + buttonText: string, + element: WebDriver | WebElement | WebElementWrapper = this.driver, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.clickByButtonText('${buttonText}') with timeout=${timeout}`); + await this.retry.try(async () => { + const button = await this.byButtonText(buttonText, element, timeout); + await button.click(); + }); + } - // Don't wrap this code in a retry, or stale element checks may get caught here and the element - // will never be re-grabbed. Let errors bubble, but continue checking for disabled property until - // it's gone. + public async clickByCssSelector( + selector: string, + timeout: number = this.defaultFindTimeout, + topOffset?: number + ): Promise { + this.log.debug(`Find.clickByCssSelector('${selector}') with timeout=${timeout}`); + await this.retry.try(async () => { const element = await this.byCssSelector(selector, timeout); - await element.moveMouseTo(); - await driver.wait(until.elementIsEnabled(element._webElement), timeout); - await element.click(); - } + if (element) { + // await element.moveMouseTo(); + await element.click(topOffset); + } else { + throw new Error(`Element with css='${selector}' is not found`); + } + }); + } - public async clickByPartialLinkText( - linkText: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.clickByPartialLinkText('${linkText}') with timeout=${timeout}`); - await retry.try(async () => { - const element = await this.byPartialLinkText(linkText, timeout); + public async clickByDisplayedLinkText( + linkText: string, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.clickByDisplayedLinkText('${linkText}') with timeout=${timeout}`); + await this.retry.try(async () => { + const element = await this.displayedByLinkText(linkText, timeout); + if (element) { await element.moveMouseTo(); await element.click(); - }); - } + } else { + throw new Error(`Element with linkText='${linkText}' is not found`); + } + }); + } - public async clickByLinkText( - linkText: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.clickByLinkText('${linkText}') with timeout=${timeout}`); - await retry.try(async () => { - const element = await this.byLinkText(linkText, timeout); + public async clickDisplayedByCssSelector( + selector: string, + timeout: number = this.defaultFindTimeout + ) { + this.log.debug(`Find.clickDisplayedByCssSelector('${selector}') with timeout=${timeout}`); + await this.retry.try(async () => { + const element = await this.displayedByCssSelector(selector, timeout); + if (element) { await element.moveMouseTo(); await element.click(); - }); - } - - public async byButtonText( - buttonText: string, - element: WebDriver | WebElement | WebElementWrapper = driver, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.byButtonText('${buttonText}') with timeout=${timeout}`); - return await retry.tryForTime(timeout, async () => { - // tslint:disable-next-line:variable-name - const _element = element instanceof WebElementWrapper ? element._webElement : element; - const allButtons = wrapAll(await _element.findElements(By.tagName('button'))); - const buttonTexts = await Promise.all( - allButtons.map(async (el) => { - return el.getVisibleText(); - }) - ); - const index = buttonTexts.findIndex((text) => text.trim() === buttonText.trim()); - if (index === -1) { - throw new Error('Button not found'); - } - return allButtons[index]; - }); - } - - public async clickByButtonText( - buttonText: string, - element: WebDriver | WebElement | WebElementWrapper = driver, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.clickByButtonText('${buttonText}') with timeout=${timeout}`); - await retry.try(async () => { - const button = await this.byButtonText(buttonText, element, timeout); - await button.click(); - }); - } + } else { + throw new Error(`Element with css='${selector}' is not found`); + } + }); + } - public async clickByCssSelector( - selector: string, - timeout: number = defaultFindTimeout, - topOffset?: number - ): Promise { - log.debug(`Find.clickByCssSelector('${selector}') with timeout=${timeout}`); - await retry.try(async () => { - const element = await this.byCssSelector(selector, timeout); - if (element) { - // await element.moveMouseTo(); - await element.click(topOffset); - } else { - throw new Error(`Element with css='${selector}' is not found`); - } - }); - } + public async waitForDeletedByCssSelector( + selector: string, + timeout: number = this.defaultFindTimeout + ) { + this.log.debug(`Find.waitForDeletedByCssSelector('${selector}') with timeout=${timeout}`); + await this._withTimeout(this.POLLING_TIME); + await this.driver.wait( + async () => { + const found = await this.driver.findElements(By.css(selector)); + return found.length === 0; + }, + timeout, + `The element ${selector} was still present when it should have disappeared.` + ); + await this._withTimeout(this.defaultFindTimeout); + } - public async clickByDisplayedLinkText( - linkText: string, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.clickByDisplayedLinkText('${linkText}') with timeout=${timeout}`); - await retry.try(async () => { - const element = await this.displayedByLinkText(linkText, timeout); - if (element) { - await element.moveMouseTo(); - await element.click(); - } else { - throw new Error(`Element with linkText='${linkText}' is not found`); - } - }); - } + public async waitForAttributeToChange( + selector: string, + attribute: string, + value: string + ): Promise { + this.log.debug(`Find.waitForAttributeToChange('${selector}', '${attribute}', '${value}')`); + await this.retry.waitFor(`${attribute} to equal "${value}"`, async () => { + const el = await this.byCssSelector(selector); + return value === (await el.getAttribute(attribute)); + }); + } - public async clickDisplayedByCssSelector( - selector: string, - timeout: number = defaultFindTimeout - ) { - log.debug(`Find.clickDisplayedByCssSelector('${selector}') with timeout=${timeout}`); - await retry.try(async () => { - const element = await this.displayedByCssSelector(selector, timeout); - if (element) { - await element.moveMouseTo(); - await element.click(); - } else { - throw new Error(`Element with css='${selector}' is not found`); - } - }); - } + public async waitForElementStale( + element: WebElementWrapper, + timeout: number = this.defaultFindTimeout + ): Promise { + this.log.debug(`Find.waitForElementStale with timeout=${timeout}`); + await this.driver.wait(until.stalenessOf(element._webElement), timeout); + } - public async waitForDeletedByCssSelector( - selector: string, - timeout: number = defaultFindTimeout - ) { - log.debug(`Find.waitForDeletedByCssSelector('${selector}') with timeout=${timeout}`); - await this._withTimeout(POLLING_TIME); - await driver.wait( - async () => { - const found = await driver.findElements(By.css(selector)); - return found.length === 0; - }, - timeout, - `The element ${selector} was still present when it should have disappeared.` - ); - await this._withTimeout(defaultFindTimeout); - } + public async waitForElementHidden( + element: WebElementWrapper, + timeout: number = this.defaultFindTimeout + ) { + this.log.debug(`Find.waitForElementHidden with timeout=${timeout}`); + await this.driver.wait(until.elementIsNotVisible(element._webElement), timeout); + } - public async waitForAttributeToChange( - selector: string, - attribute: string, - value: string - ): Promise { - log.debug(`Find.waitForAttributeToChange('${selector}', '${attribute}', '${value}')`); - await retry.waitFor(`${attribute} to equal "${value}"`, async () => { - const el = await this.byCssSelector(selector); - return value === (await el.getAttribute(attribute)); - }); + private async _withTimeout(timeout: number) { + if (timeout !== this.currentWait) { + this.currentWait = timeout; + await this.driver.manage().setTimeouts({ implicit: timeout }); } + } - public async waitForElementStale( - element: WebElementWrapper, - timeout: number = defaultFindTimeout - ): Promise { - log.debug(`Find.waitForElementStale with timeout=${timeout}`); - await driver.wait(until.stalenessOf(element._webElement), timeout); - } + private wrap(webElement: WebElement | WebElementWrapper, locator: By | null = null) { + return WebElementWrapper.create( + webElement, + locator, + this.driver, + this.defaultFindTimeout, + this.fixedHeaderHeight, + this.log, + this.browserType + ); + } - public async waitForElementHidden( - element: WebElementWrapper, - timeout: number = defaultFindTimeout - ) { - log.debug(`Find.waitForElementHidden with timeout=${timeout}`); - await driver.wait(until.elementIsNotVisible(element._webElement), timeout); - } + private wrapAll(webElements: Array) { + return webElements.map((e) => this.wrap(e)); + } - private async _withTimeout(timeout: number) { - if (timeout !== this.currentWait) { - this.currentWait = timeout; - await driver.manage().setTimeouts({ implicit: timeout }); - } - } + private async findAndWrap(locator: By, timeout: number): Promise { + const webElement = await this.driver.wait(until.elementLocated(locator), timeout); + return this.wrap(webElement, locator); } +} - return new Find(); +export async function FindProvider(ctx: FtrProviderContext) { + const { browserType, driver } = await ctx.getService('__webdriver__').init(); + return new FindService(ctx, browserType, driver); } diff --git a/test/functional/services/common/index.ts b/test/functional/services/common/index.ts index e491b964c8ff60..95f58027fd5fbc 100644 --- a/test/functional/services/common/index.ts +++ b/test/functional/services/common/index.ts @@ -9,6 +9,6 @@ export { BrowserProvider, Browser } from './browser'; export { FailureDebuggingProvider } from './failure_debugging'; export { FindProvider } from './find'; -export { ScreenshotsProvider } from './screenshots'; -export { SnapshotsProvider } from './snapshots'; -export { TestSubjectsProvider, TestSubjects } from './test_subjects'; +export { ScreenshotsService } from './screenshots'; +export { SnapshotsService } from './snapshots'; +export { TestSubjects } from './test_subjects'; diff --git a/test/functional/services/common/screenshots.ts b/test/functional/services/common/screenshots.ts index f7696901969157..79ac5a1803545c 100644 --- a/test/functional/services/common/screenshots.ts +++ b/test/functional/services/common/screenshots.ts @@ -13,77 +13,88 @@ import { promisify } from 'util'; import del from 'del'; import { comparePngs } from '../lib/compare_pngs'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext, FtrService } from '../../ftr_provider_context'; import { WebElementWrapper } from '../lib/web_element_wrapper'; const mkdirAsync = promisify(mkdir); const writeFileAsync = promisify(writeFile); -export async function ScreenshotsProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const config = getService('config'); - const failureMetadata = getService('failureMetadata'); - const browser = getService('browser'); +export class ScreenshotsService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly config = this.ctx.getService('config'); + private readonly failureMetadata = this.ctx.getService('failureMetadata'); + private readonly browser = this.ctx.getService('browser'); - const SESSION_DIRECTORY = resolve(config.get('screenshots.directory'), 'session'); - const FAILURE_DIRECTORY = resolve(config.get('screenshots.directory'), 'failure'); - const BASELINE_DIRECTORY = resolve(config.get('screenshots.directory'), 'baseline'); + private readonly SESSION_DIRECTORY = resolve(this.config.get('screenshots.directory'), 'session'); + private readonly FAILURE_DIRECTORY = resolve(this.config.get('screenshots.directory'), 'failure'); + private readonly BASELINE_DIRECTORY = resolve( + this.config.get('screenshots.directory'), + 'baseline' + ); - if (process.env.CI !== 'true' && !process.env.stack_functional_integration) { - await del([SESSION_DIRECTORY, FAILURE_DIRECTORY]); + constructor(ctx: FtrProviderContext) { + super(ctx); + + if (process.env.CI !== 'true' && !process.env.stack_functional_integration) { + ctx.getService('lifecycle').beforeTests.add(async () => { + await del([this.SESSION_DIRECTORY, this.FAILURE_DIRECTORY]); + }); + } } - class Screenshots { - /** - * - * @param name {string} name of the file to use for comparison - * @param updateBaselines {boolean} optional, pass true to update the baseline snapshot. - * @return {Promise.} Percentage difference between the baseline and the current snapshot. - */ - async compareAgainstBaseline(name: string, updateBaselines: boolean, el?: WebElementWrapper) { - log.debug('compareAgainstBaseline'); - const sessionPath = resolve(SESSION_DIRECTORY, `${name}.png`); - await this._take(sessionPath, el); + /** + * + * @param name {string} name of the file to use for comparison + * @param updateBaselines {boolean} optional, pass true to update the baseline snapshot. + * @return {Promise.} Percentage difference between the baseline and the current snapshot. + */ + async compareAgainstBaseline(name: string, updateBaselines: boolean, el?: WebElementWrapper) { + this.log.debug('compareAgainstBaseline'); + const sessionPath = resolve(this.SESSION_DIRECTORY, `${name}.png`); + await this.capture(sessionPath, el); - const baselinePath = resolve(BASELINE_DIRECTORY, `${name}.png`); - const failurePath = resolve(FAILURE_DIRECTORY, `${name}.png`); + const baselinePath = resolve(this.BASELINE_DIRECTORY, `${name}.png`); + const failurePath = resolve(this.FAILURE_DIRECTORY, `${name}.png`); - if (updateBaselines) { - log.debug('Updating baseline snapshot'); - // Make the directory if it doesn't exist - await mkdirAsync(dirname(baselinePath), { recursive: true }); - await writeFileAsync(baselinePath, readFileSync(sessionPath)); - return 0; - } else { - await mkdirAsync(FAILURE_DIRECTORY, { recursive: true }); - return await comparePngs(sessionPath, baselinePath, failurePath, SESSION_DIRECTORY, log); - } + if (updateBaselines) { + this.log.debug('Updating baseline snapshot'); + // Make the directory if it doesn't exist + await mkdirAsync(dirname(baselinePath), { recursive: true }); + await writeFileAsync(baselinePath, readFileSync(sessionPath)); + return 0; + } else { + await mkdirAsync(this.FAILURE_DIRECTORY, { recursive: true }); + return await comparePngs( + sessionPath, + baselinePath, + failurePath, + this.SESSION_DIRECTORY, + this.log + ); } + } - async take(name: string, el?: WebElementWrapper) { - const path = resolve(SESSION_DIRECTORY, `${name}.png`); - await this._take(path, el); - failureMetadata.addScreenshot(name, path); - } + async take(name: string, el?: WebElementWrapper) { + const path = resolve(this.SESSION_DIRECTORY, `${name}.png`); + await this.capture(path, el); + this.failureMetadata.addScreenshot(name, path); + } - async takeForFailure(name: string, el?: WebElementWrapper) { - const path = resolve(FAILURE_DIRECTORY, `${name}.png`); - await this._take(path, el); - failureMetadata.addScreenshot(`failure[${name}]`, path); - } + async takeForFailure(name: string, el?: WebElementWrapper) { + const path = resolve(this.FAILURE_DIRECTORY, `${name}.png`); + await this.capture(path, el); + this.failureMetadata.addScreenshot(`failure[${name}]`, path); + } - async _take(path: string, el?: WebElementWrapper) { - try { - log.info(`Taking screenshot "${path}"`); - const screenshot = await (el ? el.takeScreenshot() : browser.takeScreenshot()); - await mkdirAsync(dirname(path), { recursive: true }); - await writeFileAsync(path, screenshot, 'base64'); - } catch (err) { - log.error('SCREENSHOT FAILED'); - log.error(err); - } + private async capture(path: string, el?: WebElementWrapper) { + try { + this.log.info(`Taking screenshot "${path}"`); + const screenshot = await (el ? el.takeScreenshot() : this.browser.takeScreenshot()); + await mkdirAsync(dirname(path), { recursive: true }); + await writeFileAsync(path, screenshot, 'base64'); + } catch (err) { + this.log.error('SCREENSHOT FAILED'); + this.log.error(err); } } - - return new Screenshots(); } diff --git a/test/functional/services/common/snapshots.ts b/test/functional/services/common/snapshots.ts index 821d0aadf039e1..9f9bcb8c0305a2 100644 --- a/test/functional/services/common/snapshots.ts +++ b/test/functional/services/common/snapshots.ts @@ -12,67 +12,68 @@ import { promisify } from 'util'; import expect from '@kbn/expect'; import del from 'del'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext, FtrService } from '../../ftr_provider_context'; const mkdirAsync = promisify(mkdir); const writeFileAsync = promisify(writeFile); -export async function SnapshotsProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const config = getService('config'); +export class SnapshotsService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly config = this.ctx.getService('config'); - const SESSION_DIRECTORY = resolve(config.get('snapshots.directory'), 'session'); - const BASELINE_DIRECTORY = resolve(config.get('snapshots.directory'), 'baseline'); + private readonly SESSION_DIRECTORY = resolve(this.config.get('snapshots.directory'), 'session'); + private readonly BASELINE_DIRECTORY = resolve(this.config.get('snapshots.directory'), 'baseline'); - if (process.env.CI !== 'true' && !process.env.stack_functional_integration) { - await del([SESSION_DIRECTORY]); - } - - class Snapshots { - /** - * - * @param name {string} name of the file to use for comparison - * @param value {object} value to compare to baseline. - * @param updateBaselines {boolean} optional, pass true to update the baseline snapshot. - * @return {Promise.} returns 0 if successful. - */ - public async compareAgainstBaseline(name: string, value: object, updateBaselines?: boolean) { - log.debug('compareAgainstBaseline'); - const sessionPath = resolve(SESSION_DIRECTORY, `${name}.json`); - await this._take(sessionPath, value); - - const baselinePath = resolve(BASELINE_DIRECTORY, `${name}.json`); + constructor(ctx: FtrProviderContext) { + super(ctx); - if (updateBaselines) { - await writeFileAsync(baselinePath, readFileSync(sessionPath)); - return 0; - } else { - log.debug('comparing'); - return this.compare(sessionPath, baselinePath); - } + if (process.env.CI !== 'true' && !process.env.stack_functional_integration) { + ctx.getService('lifecycle').beforeTests.add(async () => { + await del([this.SESSION_DIRECTORY]); + }); } + } + /** + * + * @param name {string} name of the file to use for comparison + * @param value {object} value to compare to baseline. + * @param updateBaselines {boolean} optional, pass true to update the baseline snapshot. + * @return {Promise.} returns 0 if successful. + */ + public async compareAgainstBaseline(name: string, value: object, updateBaselines?: boolean) { + this.log.debug('compareAgainstBaseline'); + const sessionPath = resolve(this.SESSION_DIRECTORY, `${name}.json`); + await this._take(sessionPath, value); + + const baselinePath = resolve(this.BASELINE_DIRECTORY, `${name}.json`); - private compare(sessionPath: string, baselinePath: string) { - const currentObject = readFileSync(sessionPath, { encoding: 'utf8' }); - const baselineObject = readFileSync(baselinePath, { encoding: 'utf8' }); - expect(currentObject).to.eql(baselineObject); + if (updateBaselines) { + await writeFileAsync(baselinePath, readFileSync(sessionPath)); return 0; + } else { + this.log.debug('comparing'); + return this.compare(sessionPath, baselinePath); } + } - public async take(name: string) { - return await this._take(resolve(SESSION_DIRECTORY, `${name}.json`)); - } + private compare(sessionPath: string, baselinePath: string) { + const currentObject = readFileSync(sessionPath, { encoding: 'utf8' }); + const baselineObject = readFileSync(baselinePath, { encoding: 'utf8' }); + expect(currentObject).to.eql(baselineObject); + return 0; + } - private async _take(path: string, snapshot?: object) { - try { - await mkdirAsync(dirname(path), { recursive: true }); - await writeFileAsync(path, JSON.stringify(snapshot), 'utf8'); - } catch (err) { - log.error('SNAPSHOT FAILED'); - log.error(err); - } - } + public async take(name: string) { + return await this._take(resolve(this.SESSION_DIRECTORY, `${name}.json`)); } - return new Snapshots(); + private async _take(path: string, snapshot?: object) { + try { + await mkdirAsync(dirname(path), { recursive: true }); + await writeFileAsync(path, JSON.stringify(snapshot), 'utf8'); + } catch (err) { + this.log.error('SNAPSHOT FAILED'); + this.log.error(err); + } + } } diff --git a/test/functional/services/common/test_subjects.ts b/test/functional/services/common/test_subjects.ts index d0050859cbb32a..ae04fe5d2b9390 100644 --- a/test/functional/services/common/test_subjects.ts +++ b/test/functional/services/common/test_subjects.ts @@ -8,9 +8,8 @@ import testSubjSelector from '@kbn/test-subj-selector'; import { map as mapAsync } from 'bluebird'; -import { ProvidedType } from '@kbn/test'; import { WebElementWrapper } from '../lib/web_element_wrapper'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; interface ExistsOptions { timeout?: number; @@ -22,326 +21,329 @@ interface SetValueOptions { typeCharByChar?: boolean; } -export type TestSubjects = ProvidedType; -export function TestSubjectsProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const find = getService('find'); - const config = getService('config'); - - const FIND_TIME = config.get('timeouts.find'); - const TRY_TIME = config.get('timeouts.try'); - const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists'); - - class TestSubjects { - public async exists(selector: string, options: ExistsOptions = {}): Promise { - const { timeout = WAIT_FOR_EXISTS_TIME, allowHidden = false } = options; - - log.debug(`TestSubjects.exists(${selector})`); - return await (allowHidden - ? find.existsByCssSelector(testSubjSelector(selector), timeout) - : find.existsByDisplayedByCssSelector(testSubjSelector(selector), timeout)); - } - - public async existOrFail( - selector: string, - existsOptions?: ExistsOptions - ): Promise { - if (!(await this.exists(selector, { timeout: TRY_TIME, ...existsOptions }))) { - throw new Error(`expected testSubject(${selector}) to exist`); - } - } +export class TestSubjects extends FtrService { + public readonly log = this.ctx.getService('log'); + public readonly retry = this.ctx.getService('retry'); + public readonly findService = this.ctx.getService('find'); + public readonly config = this.ctx.getService('config'); - public async missingOrFail( - selector: string, - options: ExistsOptions = {} - ): Promise { - const { timeout = WAIT_FOR_EXISTS_TIME, allowHidden = false } = options; + public readonly FIND_TIME = this.config.get('timeouts.find'); + public readonly TRY_TIME = this.config.get('timeouts.try'); + public readonly WAIT_FOR_EXISTS_TIME = this.config.get('timeouts.waitForExists'); - log.debug(`TestSubjects.missingOrFail(${selector})`); - return await (allowHidden - ? this.waitForHidden(selector, timeout) - : find.waitForDeletedByCssSelector(testSubjSelector(selector), timeout)); - } + public async exists(selector: string, options: ExistsOptions = {}): Promise { + const { timeout = this.WAIT_FOR_EXISTS_TIME, allowHidden = false } = options; - async stringExistsInCodeBlockOrFail(codeBlockSelector: string, stringToFind: string) { - await retry.try(async () => { - const responseCodeBlock = await this.find(codeBlockSelector); - const spans = await find.allDescendantDisplayedByTagName('span', responseCodeBlock); - const foundInSpans = await Promise.all( - spans.map(async (span) => { - const text = await span.getVisibleText(); - if (text === stringToFind) { - log.debug(`"${text}" matched "${stringToFind}"!`); - return true; - } else { - log.debug(`"${text}" did not match "${stringToFind}"`); - } - }) - ); - if (!foundInSpans.find((foundInSpan) => foundInSpan)) { - throw new Error(`"${stringToFind}" was not found. Trying again...`); - } - }); - } + this.log.debug(`TestSubjects.exists(${selector})`); + return await (allowHidden + ? this.findService.existsByCssSelector(testSubjSelector(selector), timeout) + : this.findService.existsByDisplayedByCssSelector(testSubjSelector(selector), timeout)); + } - public async append(selector: string, text: string): Promise { - log.debug(`TestSubjects.append(${selector}, ${text})`); - const input = await this.find(selector); - await input.click(); - await input.type(text); + public async existOrFail(selector: string, existsOptions?: ExistsOptions): Promise { + if (!(await this.exists(selector, { timeout: this.TRY_TIME, ...existsOptions }))) { + throw new Error(`expected testSubject(${selector}) to exist`); } + } - public async clickWhenNotDisabled( - selector: string, - { timeout = FIND_TIME }: { timeout?: number } = {} - ): Promise { - log.debug(`TestSubjects.clickWhenNotDisabled(${selector})`); - await find.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), { timeout }); - } + public async missingOrFail(selector: string, options: ExistsOptions = {}): Promise { + const { timeout = this.WAIT_FOR_EXISTS_TIME, allowHidden = false } = options; - public async click( - selector: string, - timeout: number = FIND_TIME, - topOffset?: number - ): Promise { - log.debug(`TestSubjects.click(${selector})`); - await find.clickByCssSelector(testSubjSelector(selector), timeout, topOffset); - } + this.log.debug(`TestSubjects.missingOrFail(${selector})`); + return await (allowHidden + ? this.waitForHidden(selector, timeout) + : this.findService.waitForDeletedByCssSelector(testSubjSelector(selector), timeout)); + } - public async doubleClick(selector: string, timeout: number = FIND_TIME): Promise { - log.debug(`TestSubjects.doubleClick(${selector})`); - const element = await this.find(selector, timeout); - await element.moveMouseTo(); - await element.doubleClick(); - } + async stringExistsInCodeBlockOrFail(codeBlockSelector: string, stringToFind: string) { + await this.retry.try(async () => { + const responseCodeBlock = await this.find(codeBlockSelector); + const spans = await this.findService.allDescendantDisplayedByTagName( + 'span', + responseCodeBlock + ); + const foundInSpans = await Promise.all( + spans.map(async (span) => { + const text = await span.getVisibleText(); + if (text === stringToFind) { + this.log.debug(`"${text}" matched "${stringToFind}"!`); + return true; + } else { + this.log.debug(`"${text}" did not match "${stringToFind}"`); + } + }) + ); + if (!foundInSpans.find((foundInSpan) => foundInSpan)) { + throw new Error(`"${stringToFind}" was not found. Trying again...`); + } + }); + } - async descendantExists(selector: string, parentElement: WebElementWrapper): Promise { - log.debug(`TestSubjects.descendantExists(${selector})`); - return await find.descendantExistsByCssSelector(testSubjSelector(selector), parentElement); - } + public async append(selector: string, text: string): Promise { + this.log.debug(`TestSubjects.append(${selector}, ${text})`); + const input = await this.find(selector); + await input.click(); + await input.type(text); + } - public async findDescendant( - selector: string, - parentElement: WebElementWrapper - ): Promise { - log.debug(`TestSubjects.findDescendant(${selector})`); - return await find.descendantDisplayedByCssSelector(testSubjSelector(selector), parentElement); - } + public async clickWhenNotDisabled( + selector: string, + { timeout = this.FIND_TIME }: { timeout?: number } = {} + ): Promise { + this.log.debug(`TestSubjects.clickWhenNotDisabled(${selector})`); + await this.findService.clickByCssSelectorWhenNotDisabled(testSubjSelector(selector), { + timeout, + }); + } - public async findAllDescendant( - selector: string, - parentElement: WebElementWrapper - ): Promise { - log.debug(`TestSubjects.findAllDescendant(${selector})`); - return await find.allDescendantDisplayedByCssSelector( - testSubjSelector(selector), - parentElement - ); - } + public async click( + selector: string, + timeout: number = this.FIND_TIME, + topOffset?: number + ): Promise { + this.log.debug(`TestSubjects.click(${selector})`); + await this.findService.clickByCssSelector(testSubjSelector(selector), timeout, topOffset); + } - public async find(selector: string, timeout: number = FIND_TIME): Promise { - log.debug(`TestSubjects.find(${selector})`); - return await find.byCssSelector(testSubjSelector(selector), timeout); - } + public async doubleClick(selector: string, timeout: number = this.FIND_TIME): Promise { + this.log.debug(`TestSubjects.doubleClick(${selector})`); + const element = await this.find(selector, timeout); + await element.moveMouseTo(); + await element.doubleClick(); + } - public async findAll(selector: string, timeout?: number): Promise { - return await retry.try(async () => { - log.debug(`TestSubjects.findAll(${selector})`); - const all = await find.allByCssSelector(testSubjSelector(selector), timeout); - return await find.filterElementIsDisplayed(all); - }); - } + async descendantExists(selector: string, parentElement: WebElementWrapper): Promise { + this.log.debug(`TestSubjects.descendantExists(${selector})`); + return await this.findService.descendantExistsByCssSelector( + testSubjSelector(selector), + parentElement + ); + } - public async getAttributeAll(selector: string, attribute: string): Promise { - log.debug(`TestSubjects.getAttributeAll(${selector}, ${attribute})`); - return await this._mapAll(selector, async (element: WebElementWrapper) => { - return await element.getAttribute(attribute); - }); - } + public async findDescendant( + selector: string, + parentElement: WebElementWrapper + ): Promise { + this.log.debug(`TestSubjects.findDescendant(${selector})`); + return await this.findService.descendantDisplayedByCssSelector( + testSubjSelector(selector), + parentElement + ); + } - public async getAttribute( - selector: string, - attribute: string, - options?: - | number - | { - findTimeout?: number; - tryTimeout?: number; - } - ): Promise { - const findTimeout = - (typeof options === 'number' ? options : options?.findTimeout) ?? - config.get('timeouts.find'); + public async findAllDescendant( + selector: string, + parentElement: WebElementWrapper + ): Promise { + this.log.debug(`TestSubjects.findAllDescendant(${selector})`); + return await this.findService.allDescendantDisplayedByCssSelector( + testSubjSelector(selector), + parentElement + ); + } - const tryTimeout = - (typeof options !== 'number' ? options?.tryTimeout : undefined) ?? - config.get('timeouts.try'); + public async find( + selector: string, + timeout: number = this.FIND_TIME + ): Promise { + this.log.debug(`TestSubjects.find(${selector})`); + return await this.findService.byCssSelector(testSubjSelector(selector), timeout); + } - log.debug( - `TestSubjects.getAttribute(${selector}, ${attribute}, tryTimeout=${tryTimeout}, findTimeout=${findTimeout})` - ); + public async findAll(selector: string, timeout?: number): Promise { + return await this.retry.try(async () => { + this.log.debug(`TestSubjects.findAll(${selector})`); + const all = await this.findService.allByCssSelector(testSubjSelector(selector), timeout); + return await this.findService.filterElementIsDisplayed(all); + }); + } - return await retry.tryForTime(tryTimeout, async () => { - const element = await this.find(selector, findTimeout); - return await element.getAttribute(attribute); - }); - } + public async getAttributeAll(selector: string, attribute: string): Promise { + this.log.debug(`TestSubjects.getAttributeAll(${selector}, ${attribute})`); + return await this._mapAll(selector, async (element: WebElementWrapper) => { + return await element.getAttribute(attribute); + }); + } - public async setValue( - selector: string, - text: string, - options: SetValueOptions = {}, - topOffset?: number - ): Promise { - return await retry.try(async () => { - const { clearWithKeyboard = false, typeCharByChar = false } = options; - log.debug(`TestSubjects.setValue(${selector}, ${text})`); - await this.click(selector, undefined, topOffset); - // in case the input element is actually a child of the testSubject, we - // call clearValue() and type() on the element that is focused after - // clicking on the testSubject - const input = await find.activeElement(); - if (clearWithKeyboard === true) { - await input.clearValueWithKeyboard(); - } else { - await input.clearValue(); + public async getAttribute( + selector: string, + attribute: string, + options?: + | number + | { + findTimeout?: number; + tryTimeout?: number; } - await input.type(text, { charByChar: typeCharByChar }); - }); - } + ): Promise { + const findTimeout = + (typeof options === 'number' ? options : options?.findTimeout) ?? + this.config.get('timeouts.find'); + + const tryTimeout = + (typeof options !== 'number' ? options?.tryTimeout : undefined) ?? + this.config.get('timeouts.try'); + + this.log.debug( + `TestSubjects.getAttribute(${selector}, ${attribute}, tryTimeout=${tryTimeout}, findTimeout=${findTimeout})` + ); + + return await this.retry.tryForTime(tryTimeout, async () => { + const element = await this.find(selector, findTimeout); + return await element.getAttribute(attribute); + }); + } - public async selectValue(selector: string, value: string): Promise { - await find.selectValue(`[data-test-subj="${selector}"]`, value); - } + public async setValue( + selector: string, + text: string, + options: SetValueOptions = {}, + topOffset?: number + ): Promise { + return await this.retry.try(async () => { + const { clearWithKeyboard = false, typeCharByChar = false } = options; + this.log.debug(`TestSubjects.setValue(${selector}, ${text})`); + await this.click(selector, undefined, topOffset); + // in case the input element is actually a child of the testSubject, we + // call clearValue() and type() on the element that is focused after + // clicking on the testSubject + const input = await this.findService.activeElement(); + if (clearWithKeyboard === true) { + await input.clearValueWithKeyboard(); + } else { + await input.clearValue(); + } + await input.type(text, { charByChar: typeCharByChar }); + }); + } - public async isEnabled(selector: string): Promise { - log.debug(`TestSubjects.isEnabled(${selector})`); - const element = await this.find(selector); - return await element.isEnabled(); - } + public async selectValue(selector: string, value: string): Promise { + await this.findService.selectValue(`[data-test-subj="${selector}"]`, value); + } - public async isDisplayed(selector: string): Promise { - log.debug(`TestSubjects.isDisplayed(${selector})`); - const element = await this.find(selector); - return await element.isDisplayed(); - } + public async isEnabled(selector: string): Promise { + this.log.debug(`TestSubjects.isEnabled(${selector})`); + const element = await this.find(selector); + return await element.isEnabled(); + } - public async isSelected(selector: string): Promise { - log.debug(`TestSubjects.isSelected(${selector})`); - const element = await this.find(selector); + public async isDisplayed(selector: string): Promise { + this.log.debug(`TestSubjects.isDisplayed(${selector})`); + const element = await this.find(selector); + return await element.isDisplayed(); + } + + public async isSelected(selector: string): Promise { + this.log.debug(`TestSubjects.isSelected(${selector})`); + const element = await this.find(selector); + return await element.isSelected(); + } + + public async isSelectedAll(selectorAll: string): Promise { + this.log.debug(`TestSubjects.isSelectedAll(${selectorAll})`); + return await this._mapAll(selectorAll, async (element: WebElementWrapper) => { return await element.isSelected(); - } + }); + } - public async isSelectedAll(selectorAll: string): Promise { - log.debug(`TestSubjects.isSelectedAll(${selectorAll})`); - return await this._mapAll(selectorAll, async (element: WebElementWrapper) => { - return await element.isSelected(); - }); - } + public async getVisibleText(selector: string): Promise { + this.log.debug(`TestSubjects.getVisibleText(${selector})`); + const element = await this.find(selector); + return await element.getVisibleText(); + } - public async getVisibleText(selector: string): Promise { - log.debug(`TestSubjects.getVisibleText(${selector})`); - const element = await this.find(selector); + async getVisibleTextAll(selectorAll: string): Promise { + this.log.debug(`TestSubjects.getVisibleTextAll(${selectorAll})`); + return await this._mapAll(selectorAll, async (element: WebElementWrapper) => { return await element.getVisibleText(); - } + }); + } - async getVisibleTextAll(selectorAll: string): Promise { - log.debug(`TestSubjects.getVisibleTextAll(${selectorAll})`); - return await this._mapAll(selectorAll, async (element: WebElementWrapper) => { - return await element.getVisibleText(); - }); - } + public async moveMouseTo(selector: string): Promise { + // Wrapped in a retry because even though the find should do a stale element check of it's own, we seem to + // have run into a case where the element becomes stale after the find succeeds, throwing an error during the + // moveMouseTo function. + await this.retry.try(async () => { + this.log.debug(`TestSubjects.moveMouseTo(${selector})`); + const element = await this.find(selector); + await element.moveMouseTo(); + }); + } - public async moveMouseTo(selector: string): Promise { - // Wrapped in a retry because even though the find should do a stale element check of it's own, we seem to - // have run into a case where the element becomes stale after the find succeeds, throwing an error during the - // moveMouseTo function. - await retry.try(async () => { - log.debug(`TestSubjects.moveMouseTo(${selector})`); - const element = await this.find(selector); - await element.moveMouseTo(); - }); - } + private async _mapAll( + selectorAll: string, + mapFn: (element: WebElementWrapper, index?: number, arrayLength?: number) => Promise + ): Promise { + return await this.retry.try(async () => { + const elements = await this.findAll(selectorAll); + return await mapAsync(elements, mapFn); + }); + } - private async _mapAll( - selectorAll: string, - mapFn: (element: WebElementWrapper, index?: number, arrayLength?: number) => Promise - ): Promise { - return await retry.try(async () => { - const elements = await this.findAll(selectorAll); - return await mapAsync(elements, mapFn); - }); + public async waitForDeleted(selectorOrElement: string | WebElementWrapper): Promise { + if (typeof selectorOrElement === 'string') { + await this.findService.waitForDeletedByCssSelector(testSubjSelector(selectorOrElement)); + } else { + await this.findService.waitForElementStale(selectorOrElement); } + } - public async waitForDeleted(selectorOrElement: string | WebElementWrapper): Promise { - if (typeof selectorOrElement === 'string') { - await find.waitForDeletedByCssSelector(testSubjSelector(selectorOrElement)); - } else { - await find.waitForElementStale(selectorOrElement); - } - } + public async waitForAttributeToChange( + selector: string, + attribute: string, + value: string + ): Promise { + await this.findService.waitForAttributeToChange(testSubjSelector(selector), attribute, value); + } - public async waitForAttributeToChange( - selector: string, - attribute: string, - value: string - ): Promise { - await find.waitForAttributeToChange(testSubjSelector(selector), attribute, value); - } + public async waitForHidden(selector: string, timeout?: number): Promise { + this.log.debug(`TestSubjects.waitForHidden(${selector})`); + const element = await this.find(selector); + await this.findService.waitForElementHidden(element, timeout); + } - public async waitForHidden(selector: string, timeout?: number): Promise { - log.debug(`TestSubjects.waitForHidden(${selector})`); + public async waitForEnabled(selector: string, timeout: number = this.TRY_TIME): Promise { + await this.retry.tryForTime(timeout, async () => { const element = await this.find(selector); - await find.waitForElementHidden(element, timeout); - } - - public async waitForEnabled(selector: string, timeout: number = TRY_TIME): Promise { - await retry.tryForTime(timeout, async () => { - const element = await this.find(selector); - return (await element.isDisplayed()) && (await element.isEnabled()); - }); - } + return (await element.isDisplayed()) && (await element.isEnabled()); + }); + } - public getCssSelector(selector: string): string { - return testSubjSelector(selector); - } + public getCssSelector(selector: string): string { + return testSubjSelector(selector); + } - public async scrollIntoView(selector: string) { - const element = await this.find(selector); - await element.scrollIntoViewIfNecessary(); - } + public async scrollIntoView(selector: string) { + const element = await this.find(selector); + await element.scrollIntoViewIfNecessary(); + } - // isChecked always returns false when run on an euiSwitch, because they use the aria-checked attribute - public async isChecked(selector: string) { - const checkbox = await this.find(selector); - return await checkbox.isSelected(); - } + // isChecked always returns false when run on an euiSwitch, because they use the aria-checked attribute + public async isChecked(selector: string) { + const checkbox = await this.find(selector); + return await checkbox.isSelected(); + } - public async setCheckbox(selector: string, state: 'check' | 'uncheck') { - const isChecked = await this.isChecked(selector); - const states = { check: true, uncheck: false }; - if (isChecked !== states[state]) { - log.debug(`updating checkbox ${selector} from ${isChecked} to ${states[state]}`); - await this.click(selector); - } + public async setCheckbox(selector: string, state: 'check' | 'uncheck') { + const isChecked = await this.isChecked(selector); + const states = { check: true, uncheck: false }; + if (isChecked !== states[state]) { + this.log.debug(`updating checkbox ${selector} from ${isChecked} to ${states[state]}`); + await this.click(selector); } + } - public async isEuiSwitchChecked(selector: string) { - const euiSwitch = await this.find(selector); - const isChecked = await euiSwitch.getAttribute('aria-checked'); - return isChecked === 'true'; - } + public async isEuiSwitchChecked(selector: string) { + const euiSwitch = await this.find(selector); + const isChecked = await euiSwitch.getAttribute('aria-checked'); + return isChecked === 'true'; + } - public async setEuiSwitch(selector: string, state: 'check' | 'uncheck') { - const isChecked = await this.isEuiSwitchChecked(selector); - const states = { check: true, uncheck: false }; - if (isChecked !== states[state]) { - log.debug(`updating checkbox ${selector} from ${isChecked} to ${states[state]}`); - await this.click(selector); - } + public async setEuiSwitch(selector: string, state: 'check' | 'uncheck') { + const isChecked = await this.isEuiSwitchChecked(selector); + const states = { check: true, uncheck: false }; + if (isChecked !== states[state]) { + this.log.debug(`updating checkbox ${selector} from ${isChecked} to ${states[state]}`); + await this.click(selector); } } - - return new TestSubjects(); } diff --git a/test/functional/services/dashboard/add_panel.ts b/test/functional/services/dashboard/add_panel.ts index a4e0c8b2647dd8..98e947541b52d7 100644 --- a/test/functional/services/dashboard/add_panel.ts +++ b/test/functional/services/dashboard/add_panel.ts @@ -6,235 +6,233 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; - -export function DashboardAddPanelProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const flyout = getService('flyout'); - const PageObjects = getPageObjects(['header', 'common']); - - return new (class DashboardAddPanel { - async clickOpenAddPanel() { - log.debug('DashboardAddPanel.clickOpenAddPanel'); - await testSubjects.click('dashboardAddPanelButton'); - // Give some time for the animation to complete - await PageObjects.common.sleep(500); - } - - async clickCreateNewLink() { - log.debug('DashboardAddPanel.clickAddNewPanelButton'); - await testSubjects.click('dashboardAddNewPanelButton'); - // Give some time for the animation to complete - await PageObjects.common.sleep(500); - } - - async clickQuickButton(visType: string) { - log.debug(`DashboardAddPanel.clickQuickButton${visType}`); - await testSubjects.click(`dashboardQuickButton${visType}`); - } - - async clickMarkdownQuickButton() { - await this.clickQuickButton('markdown'); - } - - async clickMapQuickButton() { - await this.clickQuickButton('map'); - } - - async clickEditorMenuButton() { - log.debug('DashboardAddPanel.clickEditorMenuButton'); - await testSubjects.click('dashboardEditorMenuButton'); - } - - async clickAggBasedVisualizations() { - log.debug('DashboardAddPanel.clickEditorMenuAggBasedMenuItem'); - await testSubjects.click('dashboardEditorAggBasedMenuItem'); - } - - async clickVisType(visType: string) { - log.debug('DashboardAddPanel.clickVisType'); - await testSubjects.click(`visType-${visType}`); - } - - async clickEmbeddableFactoryGroupButton(groupId: string) { - log.debug('DashboardAddPanel.clickEmbeddableFactoryGroupButton'); - await testSubjects.click(`dashboardEditorMenu-${groupId}Group`); - } - - async clickAddNewEmbeddableLink(type: string) { - await testSubjects.click(`createNew-${type}`); - } - - async toggleFilterPopover() { - log.debug('DashboardAddPanel.toggleFilter'); - await testSubjects.click('savedObjectFinderFilterButton'); - } - - async toggleFilter(type: string) { - log.debug(`DashboardAddPanel.addToFilter(${type})`); - await this.waitForListLoading(); - await this.toggleFilterPopover(); - await testSubjects.click(`savedObjectFinderFilter-${type}`); - await this.toggleFilterPopover(); - } - - async addEveryEmbeddableOnCurrentPage() { - log.debug('addEveryEmbeddableOnCurrentPage'); - const itemList = await testSubjects.find('savedObjectFinderItemList'); - const embeddableList: string[] = []; - await retry.try(async () => { - const embeddableRows = await itemList.findAllByCssSelector('li'); - for (let i = 0; i < embeddableRows.length; i++) { - const name = await embeddableRows[i].getVisibleText(); - - if (embeddableList.includes(name)) { - // already added this one - continue; - } - - await embeddableRows[i].click(); - await PageObjects.common.closeToast(); - embeddableList.push(name); +import { FtrService } from '../../ftr_provider_context'; + +export class DashboardAddPanelService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly flyout = this.ctx.getService('flyout'); + private readonly PageObjects = this.ctx.getPageObjects(['header', 'common']); + + async clickOpenAddPanel() { + this.log.debug('DashboardAddPanel.clickOpenAddPanel'); + await this.testSubjects.click('dashboardAddPanelButton'); + // Give some time for the animation to complete + await this.PageObjects.common.sleep(500); + } + + async clickCreateNewLink() { + this.log.debug('DashboardAddPanel.clickAddNewPanelButton'); + await this.testSubjects.click('dashboardAddNewPanelButton'); + // Give some time for the animation to complete + await this.PageObjects.common.sleep(500); + } + + async clickQuickButton(visType: string) { + this.log.debug(`DashboardAddPanel.clickQuickButton${visType}`); + await this.testSubjects.click(`dashboardQuickButton${visType}`); + } + + async clickMarkdownQuickButton() { + await this.clickQuickButton('markdown'); + } + + async clickMapQuickButton() { + await this.clickQuickButton('map'); + } + + async clickEditorMenuButton() { + this.log.debug('DashboardAddPanel.clickEditorMenuButton'); + await this.testSubjects.click('dashboardEditorMenuButton'); + } + + async clickAggBasedVisualizations() { + this.log.debug('DashboardAddPanel.clickEditorMenuAggBasedMenuItem'); + await this.testSubjects.click('dashboardEditorAggBasedMenuItem'); + } + + async clickVisType(visType: string) { + this.log.debug('DashboardAddPanel.clickVisType'); + await this.testSubjects.click(`visType-${visType}`); + } + + async clickEmbeddableFactoryGroupButton(groupId: string) { + this.log.debug('DashboardAddPanel.clickEmbeddableFactoryGroupButton'); + await this.testSubjects.click(`dashboardEditorMenu-${groupId}Group`); + } + + async clickAddNewEmbeddableLink(type: string) { + await this.testSubjects.click(`createNew-${type}`); + } + + async toggleFilterPopover() { + this.log.debug('DashboardAddPanel.toggleFilter'); + await this.testSubjects.click('savedObjectFinderFilterButton'); + } + + async toggleFilter(type: string) { + this.log.debug(`DashboardAddPanel.addToFilter(${type})`); + await this.waitForListLoading(); + await this.toggleFilterPopover(); + await this.testSubjects.click(`savedObjectFinderFilter-${type}`); + await this.toggleFilterPopover(); + } + + async addEveryEmbeddableOnCurrentPage() { + this.log.debug('addEveryEmbeddableOnCurrentPage'); + const itemList = await this.testSubjects.find('savedObjectFinderItemList'); + const embeddableList: string[] = []; + await this.retry.try(async () => { + const embeddableRows = await itemList.findAllByCssSelector('li'); + for (let i = 0; i < embeddableRows.length; i++) { + const name = await embeddableRows[i].getVisibleText(); + + if (embeddableList.includes(name)) { + // already added this one + continue; } - }); - log.debug(`Added ${embeddableList.length} embeddables`); - return embeddableList; - } - - async clickPagerNextButton() { - // Clear all toasts that could hide pagination controls - await PageObjects.common.clearAllToasts(); - - const isNext = await testSubjects.exists('pagination-button-next'); - if (!isNext) { - return false; - } - - const pagerNextButton = await testSubjects.find('pagination-button-next'); - - const isDisabled = await pagerNextButton.getAttribute('disabled'); - if (isDisabled != null) { - return false; - } - - await PageObjects.header.waitUntilLoadingHasFinished(); - await pagerNextButton.click(); - await PageObjects.header.waitUntilLoadingHasFinished(); - return true; - } - - async isAddPanelOpen() { - log.debug('DashboardAddPanel.isAddPanelOpen'); - return await testSubjects.exists('dashboardAddPanel'); - } - async ensureAddPanelIsShowing() { - log.debug('DashboardAddPanel.ensureAddPanelIsShowing'); - const isOpen = await this.isAddPanelOpen(); - if (!isOpen) { - await retry.try(async () => { - await this.clickOpenAddPanel(); - const isNowOpen = await this.isAddPanelOpen(); - if (!isNowOpen) { - throw new Error('Add panel still not open, trying again.'); - } - }); + await embeddableRows[i].click(); + await this.PageObjects.common.closeToast(); + embeddableList.push(name); } + }); + this.log.debug(`Added ${embeddableList.length} embeddables`); + return embeddableList; + } + + async clickPagerNextButton() { + // Clear all toasts that could hide pagination controls + await this.PageObjects.common.clearAllToasts(); + + const isNext = await this.testSubjects.exists('pagination-button-next'); + if (!isNext) { + return false; + } + + const pagerNextButton = await this.testSubjects.find('pagination-button-next'); + + const isDisabled = await pagerNextButton.getAttribute('disabled'); + if (isDisabled != null) { + return false; + } + + await this.PageObjects.header.waitUntilLoadingHasFinished(); + await pagerNextButton.click(); + await this.PageObjects.header.waitUntilLoadingHasFinished(); + return true; + } + + async isAddPanelOpen() { + this.log.debug('DashboardAddPanel.isAddPanelOpen'); + return await this.testSubjects.exists('dashboardAddPanel'); + } + + async ensureAddPanelIsShowing() { + this.log.debug('DashboardAddPanel.ensureAddPanelIsShowing'); + const isOpen = await this.isAddPanelOpen(); + if (!isOpen) { + await this.retry.try(async () => { + await this.clickOpenAddPanel(); + const isNowOpen = await this.isAddPanelOpen(); + if (!isNowOpen) { + throw new Error('Add panel still not open, trying again.'); + } + }); } - - async waitForListLoading() { - await testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator'); - } - - async closeAddPanel() { - await flyout.ensureClosed('dashboardAddPanel'); - } - - async addEveryVisualization(filter: string) { - log.debug('DashboardAddPanel.addEveryVisualization'); - await this.ensureAddPanelIsShowing(); - await this.toggleFilter('visualization'); - if (filter) { - await this.filterEmbeddableNames(filter.replace('-', ' ')); - } - let morePages = true; - const vizList: string[][] = []; - while (morePages) { - vizList.push(await this.addEveryEmbeddableOnCurrentPage()); - morePages = await this.clickPagerNextButton(); - } - await this.closeAddPanel(); - return vizList.reduce((acc, list) => [...acc, ...list], []); - } - - async addEverySavedSearch(filter: string) { - log.debug('DashboardAddPanel.addEverySavedSearch'); - await this.ensureAddPanelIsShowing(); - await this.toggleFilter('search'); - const searchList = []; - if (filter) { - await this.filterEmbeddableNames(filter.replace('-', ' ')); - } - let morePages = true; - while (morePages) { - searchList.push(await this.addEveryEmbeddableOnCurrentPage()); - morePages = await this.clickPagerNextButton(); - } - await this.closeAddPanel(); - return searchList.reduce((acc, list) => [...acc, ...list], []); - } - - async addSavedSearch(searchName: string) { - return this.addEmbeddable(searchName, 'search'); - } - - async addSavedSearches(searches: string[]) { - for (const name of searches) { - await this.addSavedSearch(name); - } - } - - async addVisualizations(visualizations: string[]) { - log.debug('DashboardAddPanel.addVisualizations'); - const vizList = []; - for (const vizName of visualizations) { - await this.addVisualization(vizName); - vizList.push(vizName); - } - return vizList; - } - - async addVisualization(vizName: string) { - return this.addEmbeddable(vizName, 'visualization'); - } - - async addEmbeddable(embeddableName: string, embeddableType: string) { - log.debug( - `DashboardAddPanel.addEmbeddable, name: ${embeddableName}, type: ${embeddableType}` - ); - await this.ensureAddPanelIsShowing(); - await this.toggleFilter(embeddableType); - await this.filterEmbeddableNames(`"${embeddableName.replace('-', ' ')}"`); - await testSubjects.click(`savedObjectTitle${embeddableName.split(' ').join('-')}`); - await testSubjects.exists('addObjectToDashboardSuccess'); - await this.closeAddPanel(); - return embeddableName; - } - - async filterEmbeddableNames(name: string) { - // The search input field may be disabled while the table is loading so wait for it - await this.waitForListLoading(); - await testSubjects.setValue('savedObjectFinderSearchInput', name); - await this.waitForListLoading(); - } - - async panelAddLinkExists(name: string) { - log.debug(`DashboardAddPanel.panelAddLinkExists(${name})`); - await this.ensureAddPanelIsShowing(); - await this.filterEmbeddableNames(`"${name}"`); - return await testSubjects.exists(`savedObjectTitle${name.split(' ').join('-')}`); - } - })(); + } + + async waitForListLoading() { + await this.testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator'); + } + + async closeAddPanel() { + await this.flyout.ensureClosed('dashboardAddPanel'); + } + + async addEveryVisualization(filter: string) { + this.log.debug('DashboardAddPanel.addEveryVisualization'); + await this.ensureAddPanelIsShowing(); + await this.toggleFilter('visualization'); + if (filter) { + await this.filterEmbeddableNames(filter.replace('-', ' ')); + } + let morePages = true; + const vizList: string[][] = []; + while (morePages) { + vizList.push(await this.addEveryEmbeddableOnCurrentPage()); + morePages = await this.clickPagerNextButton(); + } + await this.closeAddPanel(); + return vizList.reduce((acc, list) => [...acc, ...list], []); + } + + async addEverySavedSearch(filter: string) { + this.log.debug('DashboardAddPanel.addEverySavedSearch'); + await this.ensureAddPanelIsShowing(); + await this.toggleFilter('search'); + const searchList = []; + if (filter) { + await this.filterEmbeddableNames(filter.replace('-', ' ')); + } + let morePages = true; + while (morePages) { + searchList.push(await this.addEveryEmbeddableOnCurrentPage()); + morePages = await this.clickPagerNextButton(); + } + await this.closeAddPanel(); + return searchList.reduce((acc, list) => [...acc, ...list], []); + } + + async addSavedSearch(searchName: string) { + return this.addEmbeddable(searchName, 'search'); + } + + async addSavedSearches(searches: string[]) { + for (const name of searches) { + await this.addSavedSearch(name); + } + } + + async addVisualizations(visualizations: string[]) { + this.log.debug('DashboardAddPanel.addVisualizations'); + const vizList = []; + for (const vizName of visualizations) { + await this.addVisualization(vizName); + vizList.push(vizName); + } + return vizList; + } + + async addVisualization(vizName: string) { + return this.addEmbeddable(vizName, 'visualization'); + } + + async addEmbeddable(embeddableName: string, embeddableType: string) { + this.log.debug( + `DashboardAddPanel.addEmbeddable, name: ${embeddableName}, type: ${embeddableType}` + ); + await this.ensureAddPanelIsShowing(); + await this.toggleFilter(embeddableType); + await this.filterEmbeddableNames(`"${embeddableName.replace('-', ' ')}"`); + await this.testSubjects.click(`savedObjectTitle${embeddableName.split(' ').join('-')}`); + await this.testSubjects.exists('addObjectToDashboardSuccess'); + await this.closeAddPanel(); + return embeddableName; + } + + async filterEmbeddableNames(name: string) { + // The search input field may be disabled while the table is loading so wait for it + await this.waitForListLoading(); + await this.testSubjects.setValue('savedObjectFinderSearchInput', name); + await this.waitForListLoading(); + } + + async panelAddLinkExists(name: string) { + this.log.debug(`DashboardAddPanel.panelAddLinkExists(${name})`); + await this.ensureAddPanelIsShowing(); + await this.filterEmbeddableNames(`"${name}"`); + return await this.testSubjects.exists(`savedObjectTitle${name.split(' ').join('-')}`); + } } diff --git a/test/functional/services/dashboard/expectations.ts b/test/functional/services/dashboard/expectations.ts index c58fdd4d0305b1..34a4a9de7899a9 100644 --- a/test/functional/services/dashboard/expectations.ts +++ b/test/functional/services/dashboard/expectations.ts @@ -7,263 +7,271 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; import { WebElementWrapper } from '../lib/web_element_wrapper'; -export function DashboardExpectProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const filterBar = getService('filterBar'); - const PageObjects = getPageObjects(['dashboard', 'visualize', 'visChart']); - const findTimeout = 2500; +export class DashboardExpectService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly find = this.ctx.getService('find'); + private readonly filterBar = this.ctx.getService('filterBar'); + private readonly PageObjects = this.ctx.getPageObjects(['dashboard', 'visualize', 'visChart']); + private readonly findTimeout = 2500; - return new (class DashboardExpect { - async panelCount(expectedCount: number) { - log.debug(`DashboardExpect.panelCount(${expectedCount})`); - await retry.try(async () => { - const panelCount = await PageObjects.dashboard.getPanelCount(); - expect(panelCount).to.be(expectedCount); - }); - } + async panelCount(expectedCount: number) { + this.log.debug(`DashboardExpect.panelCount(${expectedCount})`); + await this.retry.try(async () => { + const panelCount = await this.PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.be(expectedCount); + }); + } - async visualizationsArePresent(vizList: string[]) { - log.debug('Checking all visualisations are present on dashsboard'); - let notLoaded = await PageObjects.dashboard.getNotLoadedVisualizations(vizList); - // TODO: Determine issue occasionally preventing 'geo map' from loading - notLoaded = notLoaded.filter((x) => x !== 'Rendering Test: geo map'); - expect(notLoaded).to.be.empty(); - } + async visualizationsArePresent(vizList: string[]) { + this.log.debug('Checking all visualisations are present on dashsboard'); + let notLoaded = await this.PageObjects.dashboard.getNotLoadedVisualizations(vizList); + // TODO: Determine issue occasionally preventing 'geo map' from loading + notLoaded = notLoaded.filter((x) => x !== 'Rendering Test: geo map'); + expect(notLoaded).to.be.empty(); + } - async selectedLegendColorCount(color: string, expectedCount: number) { - log.debug(`DashboardExpect.selectedLegendColorCount(${color}, ${expectedCount})`); - await retry.try(async () => { - const selectedLegendColor = await testSubjects.findAll( - `legendSelectedColor-${color}`, - findTimeout - ); - expect(selectedLegendColor.length).to.be(expectedCount); - }); - } + async selectedLegendColorCount(color: string, expectedCount: number) { + this.log.debug(`DashboardExpect.selectedLegendColorCount(${color}, ${expectedCount})`); + await this.retry.try(async () => { + const selectedLegendColor = await this.testSubjects.findAll( + `legendSelectedColor-${color}`, + this.findTimeout + ); + expect(selectedLegendColor.length).to.be(expectedCount); + }); + } - async docTableFieldCount(expectedCount: number) { - log.debug(`DashboardExpect.docTableFieldCount(${expectedCount})`); - await retry.try(async () => { - const docTableCells = await testSubjects.findAll('docTableField', findTimeout); - expect(docTableCells.length).to.be(expectedCount); - }); - } + async docTableFieldCount(expectedCount: number) { + this.log.debug(`DashboardExpect.docTableFieldCount(${expectedCount})`); + await this.retry.try(async () => { + const docTableCells = await this.testSubjects.findAll('docTableField', this.findTimeout); + expect(docTableCells.length).to.be(expectedCount); + }); + } - async fieldSuggestions(expectedFields: string[]) { - log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`); - const fields = await filterBar.getFilterEditorFields(); - expectedFields.forEach((expectedField) => { - expect(fields).to.contain(expectedField); - }); - } + async fieldSuggestions(expectedFields: string[]) { + this.log.debug(`DashboardExpect.fieldSuggestions(${expectedFields})`); + const fields = await this.filterBar.getFilterEditorFields(); + expectedFields.forEach((expectedField) => { + expect(fields).to.contain(expectedField); + }); + } + + async legendValuesToExist(legendValues: string[]) { + this.log.debug(`DashboardExpect.legendValuesToExist(${legendValues})`); + await Promise.all( + legendValues.map(async (legend) => { + await this.retry.try(async () => { + const legendValueExists = await this.testSubjects.exists(`legend-${legend}`); + expect(legendValueExists).to.be(true); + }); + }) + ); + } - async legendValuesToExist(legendValues: string[]) { - log.debug(`DashboardExpect.legendValuesToExist(${legendValues})`); + async textWithinElementsExists(texts: string[], getElementsFn: Function) { + this.log.debug(`DashboardExpect.textWithinElementsExists(${texts})`); + await this.retry.try(async () => { + const elements: WebElementWrapper[] = await getElementsFn(); + const elementTexts: string[] = []; await Promise.all( - legendValues.map(async (legend) => { - await retry.try(async () => { - const legendValueExists = await testSubjects.exists(`legend-${legend}`); - expect(legendValueExists).to.be(true); - }); + elements.map(async (element) => { + elementTexts.push(await element.getVisibleText()); }) ); - } - - async textWithinElementsExists(texts: string[], getElementsFn: Function) { - log.debug(`DashboardExpect.textWithinElementsExists(${texts})`); - await retry.try(async () => { - const elements: WebElementWrapper[] = await getElementsFn(); - const elementTexts: string[] = []; - await Promise.all( - elements.map(async (element) => { - elementTexts.push(await element.getVisibleText()); - }) - ); - log.debug(`Found ${elements.length} elements with values: ${JSON.stringify(elementTexts)}`); - texts.forEach((value) => { - const indexOfValue = elementTexts.indexOf(value); - expect(indexOfValue).to.be.greaterThan(-1); - elementTexts.splice(indexOfValue, 1); - }); + this.log.debug( + `Found ${elements.length} elements with values: ${JSON.stringify(elementTexts)}` + ); + texts.forEach((value) => { + const indexOfValue = elementTexts.indexOf(value); + expect(indexOfValue).to.be.greaterThan(-1); + elementTexts.splice(indexOfValue, 1); }); - } + }); + } - async textWithinTestSubjectsExists(texts: string[], selector: string) { - log.debug(`DashboardExpect.textWithinTestSubjectsExists(${texts})`); - log.debug(`textWithinTestSubjectsExists:(${JSON.stringify(texts)},${selector})`); - await this.textWithinElementsExists(texts, async () => await testSubjects.findAll(selector)); - } + async textWithinTestSubjectsExists(texts: string[], selector: string) { + this.log.debug(`DashboardExpect.textWithinTestSubjectsExists(${texts})`); + this.log.debug(`textWithinTestSubjectsExists:(${JSON.stringify(texts)},${selector})`); + await this.textWithinElementsExists( + texts, + async () => await this.testSubjects.findAll(selector) + ); + } - async textWithinCssElementExists(texts: string[], selector: string) { - log.debug(`DashboardExpect.textWithinCssElementExists(${texts})`); - log.debug(`textWithinCssElementExists:(${JSON.stringify(texts)},${selector})`); - await this.textWithinElementsExists(texts, async () => await find.allByCssSelector(selector)); - } + async textWithinCssElementExists(texts: string[], selector: string) { + this.log.debug(`DashboardExpect.textWithinCssElementExists(${texts})`); + this.log.debug(`textWithinCssElementExists:(${JSON.stringify(texts)},${selector})`); + await this.textWithinElementsExists( + texts, + async () => await this.find.allByCssSelector(selector) + ); + } - async textWithinElementsDoNotExist(texts: string[], getElementsFn: Function) { - log.debug(`DashboardExpect.textWithinElementsDoNotExist(${texts})`); - await retry.try(async () => { - const elements: WebElementWrapper[] = await getElementsFn(); - const elementTexts: string[] = []; - await Promise.all( - elements.map(async (element) => { - elementTexts.push(await element.getVisibleText()); - }) - ); - log.debug(`Found ${elements.length} elements with values: ${JSON.stringify(elementTexts)}`); - texts.forEach((value) => { - const indexOfValue = elementTexts.indexOf(value); - expect(indexOfValue).to.be(-1); - }); + async textWithinElementsDoNotExist(texts: string[], getElementsFn: Function) { + this.log.debug(`DashboardExpect.textWithinElementsDoNotExist(${texts})`); + await this.retry.try(async () => { + const elements: WebElementWrapper[] = await getElementsFn(); + const elementTexts: string[] = []; + await Promise.all( + elements.map(async (element) => { + elementTexts.push(await element.getVisibleText()); + }) + ); + this.log.debug( + `Found ${elements.length} elements with values: ${JSON.stringify(elementTexts)}` + ); + texts.forEach((value) => { + const indexOfValue = elementTexts.indexOf(value); + expect(indexOfValue).to.be(-1); }); - } + }); + } - async textWithinCssElementDoNotExist(texts: string[], selector: string) { - log.debug(`textWithinCssElementExists:(${JSON.stringify(texts)},${selector})`); - await this.textWithinElementsDoNotExist( - texts, - async () => await find.allByCssSelector(selector) - ); - } + async textWithinCssElementDoNotExist(texts: string[], selector: string) { + this.log.debug(`textWithinCssElementExists:(${JSON.stringify(texts)},${selector})`); + await this.textWithinElementsDoNotExist( + texts, + async () => await this.find.allByCssSelector(selector) + ); + } - async timelionLegendCount(expectedCount: number) { - log.debug(`DashboardExpect.timelionLegendCount(${expectedCount})`); - await retry.try(async () => { - const flotLegendLabels = await testSubjects.findAll('flotLegendLabel', findTimeout); - expect(flotLegendLabels.length).to.be(expectedCount); - }); - } + async timelionLegendCount(expectedCount: number) { + this.log.debug(`DashboardExpect.timelionLegendCount(${expectedCount})`); + await this.retry.try(async () => { + const flotLegendLabels = await this.testSubjects.findAll('flotLegendLabel', this.findTimeout); + expect(flotLegendLabels.length).to.be(expectedCount); + }); + } - async emptyTagCloudFound() { - log.debug(`DashboardExpect.emptyTagCloudFound()`); - const tagCloudVisualizations = await testSubjects.findAll('tagCloudVisualization'); - const tagCloudsHaveContent = await Promise.all( - tagCloudVisualizations.map(async (tagCloud) => { - return await find.descendantExistsByCssSelector('text', tagCloud); - }) - ); - expect(tagCloudsHaveContent.indexOf(false)).to.be.greaterThan(-1); - } + async emptyTagCloudFound() { + this.log.debug(`DashboardExpect.emptyTagCloudFound()`); + const tagCloudVisualizations = await this.testSubjects.findAll('tagCloudVisualization'); + const tagCloudsHaveContent = await Promise.all( + tagCloudVisualizations.map(async (tagCloud) => { + return await this.find.descendantExistsByCssSelector('text', tagCloud); + }) + ); + expect(tagCloudsHaveContent.indexOf(false)).to.be.greaterThan(-1); + } - async tagCloudWithValuesFound(values: string[]) { - log.debug(`DashboardExpect.tagCloudWithValuesFound(${values})`); - const tagCloudVisualizations = await testSubjects.findAll('tagCloudVisualization'); - const matches = await Promise.all( - tagCloudVisualizations.map(async (tagCloud) => { - for (let i = 0; i < values.length; i++) { - const valueExists = await testSubjects.descendantExists(values[i], tagCloud); - if (!valueExists) { - return false; - } + async tagCloudWithValuesFound(values: string[]) { + this.log.debug(`DashboardExpect.tagCloudWithValuesFound(${values})`); + const tagCloudVisualizations = await this.testSubjects.findAll('tagCloudVisualization'); + const matches = await Promise.all( + tagCloudVisualizations.map(async (tagCloud) => { + for (let i = 0; i < values.length; i++) { + const valueExists = await this.testSubjects.descendantExists(values[i], tagCloud); + if (!valueExists) { + return false; } - return true; - }) - ); - expect(matches.indexOf(true)).to.be.greaterThan(-1); - } + } + return true; + }) + ); + expect(matches.indexOf(true)).to.be.greaterThan(-1); + } - async goalAndGuageLabelsExist(labels: string[]) { - log.debug(`DashboardExpect.goalAndGuageLabelsExist(${labels})`); - await this.textWithinCssElementExists(labels, '.chart-label'); - } + async goalAndGuageLabelsExist(labels: string[]) { + this.log.debug(`DashboardExpect.goalAndGuageLabelsExist(${labels})`); + await this.textWithinCssElementExists(labels, '.chart-label'); + } - async metricValuesExist(values: string[]) { - log.debug(`DashboardExpect.metricValuesExist(${values})`); - await this.textWithinCssElementExists(values, '.mtrVis__value'); - } + async metricValuesExist(values: string[]) { + this.log.debug(`DashboardExpect.metricValuesExist(${values})`); + await this.textWithinCssElementExists(values, '.mtrVis__value'); + } - async tsvbMetricValuesExist(values: string[]) { - log.debug(`DashboardExpect.tsvbMetricValuesExist(${values})`); - await this.textWithinTestSubjectsExists(values, 'tsvbMetricValue'); - } + async tsvbMetricValuesExist(values: string[]) { + this.log.debug(`DashboardExpect.tsvbMetricValuesExist(${values})`); + await this.textWithinTestSubjectsExists(values, 'tsvbMetricValue'); + } - async tsvbTopNValuesExist(values: string[]) { - log.debug(`DashboardExpect.tsvbTopNValuesExist(${values})`); - await this.textWithinTestSubjectsExists(values, 'tsvbTopNValue'); - } + async tsvbTopNValuesExist(values: string[]) { + this.log.debug(`DashboardExpect.tsvbTopNValuesExist(${values})`); + await this.textWithinTestSubjectsExists(values, 'tsvbTopNValue'); + } - async vegaTextsExist(values: string[]) { - log.debug(`DashboardExpect.vegaTextsExist(${values})`); - await this.textWithinCssElementExists(values, '.vgaVis__view text'); - } + async vegaTextsExist(values: string[]) { + this.log.debug(`DashboardExpect.vegaTextsExist(${values})`); + await this.textWithinCssElementExists(values, '.vgaVis__view text'); + } - async vegaTextsDoNotExist(values: string[]) { - log.debug(`DashboardExpect.vegaTextsDoNotExist(${values})`); - await this.textWithinCssElementDoNotExist(values, '.vgaVis__view text'); - } + async vegaTextsDoNotExist(values: string[]) { + this.log.debug(`DashboardExpect.vegaTextsDoNotExist(${values})`); + await this.textWithinCssElementDoNotExist(values, '.vgaVis__view text'); + } - async tsvbMarkdownWithValuesExists(values: string[]) { - log.debug(`DashboardExpect.tsvbMarkdownWithValuesExists(${values})`); - await this.textWithinTestSubjectsExists(values, 'tsvbMarkdown'); - } + async tsvbMarkdownWithValuesExists(values: string[]) { + this.log.debug(`DashboardExpect.tsvbMarkdownWithValuesExists(${values})`); + await this.textWithinTestSubjectsExists(values, 'tsvbMarkdown'); + } - async markdownWithValuesExists(values: string[]) { - log.debug(`DashboardExpect.markdownWithValuesExists(${values})`); - await this.textWithinTestSubjectsExists(values, 'markdownBody'); - } + async markdownWithValuesExists(values: string[]) { + this.log.debug(`DashboardExpect.markdownWithValuesExists(${values})`); + await this.textWithinTestSubjectsExists(values, 'markdownBody'); + } - async savedSearchRowCount(expectedMinCount: number) { - log.debug(`DashboardExpect.savedSearchRowCount(${expectedMinCount})`); - await retry.try(async () => { - const savedSearchRows = await testSubjects.findAll( - 'docTableExpandToggleColumn', - findTimeout - ); - expect(savedSearchRows.length).to.be.above(expectedMinCount); - }); - } + async savedSearchRowCount(expectedMinCount: number) { + this.log.debug(`DashboardExpect.savedSearchRowCount(${expectedMinCount})`); + await this.retry.try(async () => { + const savedSearchRows = await this.testSubjects.findAll( + 'docTableExpandToggleColumn', + this.findTimeout + ); + expect(savedSearchRows.length).to.be.above(expectedMinCount); + }); + } - async dataTableRowCount(expectedCount: number) { - log.debug(`DashboardExpect.dataTableRowCount(${expectedCount})`); - await retry.try(async () => { - const dataTableRows = await PageObjects.visChart.getTableVisContent(); - expect(dataTableRows.length).to.be(expectedCount); - }); - } + async dataTableRowCount(expectedCount: number) { + this.log.debug(`DashboardExpect.dataTableRowCount(${expectedCount})`); + await this.retry.try(async () => { + const dataTableRows = await this.PageObjects.visChart.getTableVisContent(); + expect(dataTableRows.length).to.be(expectedCount); + }); + } - async dataTableNoResult() { - log.debug(`DashboardExpect.dataTableNoResult`); - await retry.try(async () => { - await PageObjects.visChart.getTableVisNoResult(); - }); - } + async dataTableNoResult() { + this.log.debug(`DashboardExpect.dataTableNoResult`); + await this.retry.try(async () => { + await this.PageObjects.visChart.getTableVisNoResult(); + }); + } - async seriesElementCount(expectedCount: number) { - log.debug(`DashboardExpect.seriesElementCount(${expectedCount})`); - await retry.try(async () => { - const seriesElements = await find.allByCssSelector('.series', findTimeout); - expect(seriesElements.length).to.be(expectedCount); - }); - } + async seriesElementCount(expectedCount: number) { + this.log.debug(`DashboardExpect.seriesElementCount(${expectedCount})`); + await this.retry.try(async () => { + const seriesElements = await this.find.allByCssSelector('.series', this.findTimeout); + expect(seriesElements.length).to.be(expectedCount); + }); + } - async inputControlItemCount(expectedCount: number) { - log.debug(`DashboardExpect.inputControlItemCount(${expectedCount})`); - await retry.try(async () => { - const inputControlItems = await testSubjects.findAll('inputControlItem'); - expect(inputControlItems.length).to.be(expectedCount); - }); - } + async inputControlItemCount(expectedCount: number) { + this.log.debug(`DashboardExpect.inputControlItemCount(${expectedCount})`); + await this.retry.try(async () => { + const inputControlItems = await this.testSubjects.findAll('inputControlItem'); + expect(inputControlItems.length).to.be(expectedCount); + }); + } - async lineChartPointsCount(expectedCount: number) { - log.debug(`DashboardExpect.lineChartPointsCount(${expectedCount})`); - await retry.try(async () => { - const points = await find.allByCssSelector('.points', findTimeout); - expect(points.length).to.be(expectedCount); - }); - } + async lineChartPointsCount(expectedCount: number) { + this.log.debug(`DashboardExpect.lineChartPointsCount(${expectedCount})`); + await this.retry.try(async () => { + const points = await this.find.allByCssSelector('.points', this.findTimeout); + expect(points.length).to.be(expectedCount); + }); + } - async tsvbTableCellCount(expectedCount: number) { - log.debug(`DashboardExpect.tsvbTableCellCount(${expectedCount})`); - await retry.try(async () => { - const tableCells = await testSubjects.findAll('tvbTableVis__value', findTimeout); - expect(tableCells.length).to.be(expectedCount); - }); - } - })(); + async tsvbTableCellCount(expectedCount: number) { + this.log.debug(`DashboardExpect.tsvbTableCellCount(${expectedCount})`); + await this.retry.try(async () => { + const tableCells = await this.testSubjects.findAll('tvbTableVis__value', this.findTimeout); + expect(tableCells.length).to.be(expectedCount); + }); + } } diff --git a/test/functional/services/dashboard/index.ts b/test/functional/services/dashboard/index.ts index 2746718892cf03..0423b66d04102b 100644 --- a/test/functional/services/dashboard/index.ts +++ b/test/functional/services/dashboard/index.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -export { DashboardVisualizationProvider } from './visualizations'; -export { DashboardExpectProvider } from './expectations'; -export { DashboardAddPanelProvider } from './add_panel'; -export { DashboardReplacePanelProvider } from './replace_panel'; -export { DashboardPanelActionsProvider } from './panel_actions'; +export { DashboardVisualizationsService } from './visualizations'; +export { DashboardExpectService } from './expectations'; +export { DashboardAddPanelService } from './add_panel'; +export { DashboardReplacePanelService } from './replace_panel'; +export { DashboardPanelActionsService } from './panel_actions'; diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 89790b19f426aa..e7c028acc0e1bf 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; import { WebElementWrapper } from '../lib/web_element_wrapper'; +import { FtrService } from '../../ftr_provider_context'; const REMOVE_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-deletePanel'; const EDIT_PANEL_DATA_TEST_SUBJ = 'embeddablePanelAction-editPanel'; @@ -21,320 +21,318 @@ const COPY_PANEL_TO_DATA_TEST_SUBJ = 'embeddablePanelAction-copyToDashboard'; const LIBRARY_NOTIFICATION_TEST_SUBJ = 'embeddablePanelNotification-ACTION_LIBRARY_NOTIFICATION'; const SAVE_TO_LIBRARY_TEST_SUBJ = 'embeddablePanelAction-saveToLibrary'; -export function DashboardPanelActionsProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['header', 'common', 'dashboard']); - const inspector = getService('inspector'); - - return new (class DashboardPanelActions { - async findContextMenu(parent?: WebElementWrapper) { - return parent - ? await testSubjects.findDescendant(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ, parent) - : await testSubjects.find(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); - } - - async isContextMenuIconVisible() { - log.debug('isContextMenuIconVisible'); - return await testSubjects.exists(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); - } - - async toggleContextMenu(parent?: WebElementWrapper) { - log.debug('toggleContextMenu'); - await (parent ? parent.moveMouseTo() : testSubjects.moveMouseTo('dashboardPanelTitle')); - const toggleMenuItem = await this.findContextMenu(parent); - await toggleMenuItem.click(); - } - - async expectContextMenuToBeOpen() { - await testSubjects.existOrFail('embeddablePanelContextMenuOpen'); - } - - async openContextMenu(parent?: WebElementWrapper) { - log.debug(`openContextMenu(${parent}`); - if (await testSubjects.exists('embeddablePanelContextMenuOpen')) return; - await this.toggleContextMenu(parent); - await this.expectContextMenuToBeOpen(); - } - - async hasContextMenuMoreItem() { - return await testSubjects.exists('embeddablePanelMore-mainMenu'); - } - - async clickContextMenuMoreItem() { - const hasMoreSubPanel = await testSubjects.exists('embeddablePanelMore-mainMenu'); - if (hasMoreSubPanel) { - await testSubjects.click('embeddablePanelMore-mainMenu'); - } - } - - async openContextMenuMorePanel(parent?: WebElementWrapper) { - await this.openContextMenu(parent); - await this.clickContextMenuMoreItem(); - } - - async clickEdit() { - log.debug('clickEdit'); +export class DashboardPanelActionsService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly inspector = this.ctx.getService('inspector'); + private readonly PageObjects = this.ctx.getPageObjects(['header', 'common', 'dashboard']); + + async findContextMenu(parent?: WebElementWrapper) { + return parent + ? await this.testSubjects.findDescendant(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ, parent) + : await this.testSubjects.find(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); + } + + async isContextMenuIconVisible() { + this.log.debug('isContextMenuIconVisible'); + return await this.testSubjects.exists(OPEN_CONTEXT_MENU_ICON_DATA_TEST_SUBJ); + } + + async toggleContextMenu(parent?: WebElementWrapper) { + this.log.debug('toggleContextMenu'); + await (parent ? parent.moveMouseTo() : this.testSubjects.moveMouseTo('dashboardPanelTitle')); + const toggleMenuItem = await this.findContextMenu(parent); + await toggleMenuItem.click(); + } + + async expectContextMenuToBeOpen() { + await this.testSubjects.existOrFail('embeddablePanelContextMenuOpen'); + } + + async openContextMenu(parent?: WebElementWrapper) { + this.log.debug(`openContextMenu(${parent}`); + if (await this.testSubjects.exists('embeddablePanelContextMenuOpen')) return; + await this.toggleContextMenu(parent); + await this.expectContextMenuToBeOpen(); + } + + async hasContextMenuMoreItem() { + return await this.testSubjects.exists('embeddablePanelMore-mainMenu'); + } + + async clickContextMenuMoreItem() { + const hasMoreSubPanel = await this.testSubjects.exists('embeddablePanelMore-mainMenu'); + if (hasMoreSubPanel) { + await this.testSubjects.click('embeddablePanelMore-mainMenu'); + } + } + + async openContextMenuMorePanel(parent?: WebElementWrapper) { + await this.openContextMenu(parent); + await this.clickContextMenuMoreItem(); + } + + async clickEdit() { + this.log.debug('clickEdit'); + await this.openContextMenu(); + const isActionVisible = await this.testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ); + if (!isActionVisible) await this.clickContextMenuMoreItem(); + await this.testSubjects.clickWhenNotDisabled(EDIT_PANEL_DATA_TEST_SUBJ); + await this.PageObjects.header.waitUntilLoadingHasFinished(); + await this.PageObjects.common.waitForTopNavToBeVisible(); + } + + async editPanelByTitle(title?: string) { + this.log.debug(`editPanelByTitle(${title})`); + if (title) { + const panelOptions = await this.getPanelHeading(title); + await this.openContextMenu(panelOptions); + } else { await this.openContextMenu(); - const isActionVisible = await testSubjects.exists(EDIT_PANEL_DATA_TEST_SUBJ); - if (!isActionVisible) await this.clickContextMenuMoreItem(); - await testSubjects.clickWhenNotDisabled(EDIT_PANEL_DATA_TEST_SUBJ); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.common.waitForTopNavToBeVisible(); - } - - async editPanelByTitle(title?: string) { - log.debug(`editPanelByTitle(${title})`); - if (title) { - const panelOptions = await this.getPanelHeading(title); - await this.openContextMenu(panelOptions); - } else { - await this.openContextMenu(); - } - await testSubjects.clickWhenNotDisabled(EDIT_PANEL_DATA_TEST_SUBJ); } - - async clickExpandPanelToggle() { - log.debug(`clickExpandPanelToggle`); + await this.testSubjects.clickWhenNotDisabled(EDIT_PANEL_DATA_TEST_SUBJ); + } + + async clickExpandPanelToggle() { + this.log.debug(`clickExpandPanelToggle`); + await this.openContextMenu(); + const isActionVisible = await this.testSubjects.exists(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + if (!isActionVisible) await this.clickContextMenuMoreItem(); + await this.testSubjects.click(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + } + + async removePanel(parent?: WebElementWrapper) { + this.log.debug('removePanel'); + await this.openContextMenu(parent); + const isActionVisible = await this.testSubjects.exists(REMOVE_PANEL_DATA_TEST_SUBJ); + if (!isActionVisible) await this.clickContextMenuMoreItem(); + const isPanelActionVisible = await this.testSubjects.exists(REMOVE_PANEL_DATA_TEST_SUBJ); + if (!isPanelActionVisible) await this.clickContextMenuMoreItem(); + await this.testSubjects.click(REMOVE_PANEL_DATA_TEST_SUBJ); + } + + async removePanelByTitle(title: string) { + const header = await this.getPanelHeading(title); + this.log.debug('found header? ', Boolean(header)); + await this.removePanel(header); + } + + async customizePanel(parent?: WebElementWrapper) { + await this.openContextMenu(parent); + await this.testSubjects.click(CUSTOMIZE_PANEL_DATA_TEST_SUBJ); + } + + async replacePanelByTitle(title?: string) { + this.log.debug(`replacePanel(${title})`); + if (title) { + const panelOptions = await this.getPanelHeading(title); + await this.openContextMenu(panelOptions); + } else { await this.openContextMenu(); - const isActionVisible = await testSubjects.exists(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); - if (!isActionVisible) await this.clickContextMenuMoreItem(); - await testSubjects.click(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); - } - - async removePanel(parent?: WebElementWrapper) { - log.debug('removePanel'); - await this.openContextMenu(parent); - const isActionVisible = await testSubjects.exists(REMOVE_PANEL_DATA_TEST_SUBJ); - if (!isActionVisible) await this.clickContextMenuMoreItem(); - const isPanelActionVisible = await testSubjects.exists(REMOVE_PANEL_DATA_TEST_SUBJ); - if (!isPanelActionVisible) await this.clickContextMenuMoreItem(); - await testSubjects.click(REMOVE_PANEL_DATA_TEST_SUBJ); - } - - async removePanelByTitle(title: string) { - const header = await this.getPanelHeading(title); - log.debug('found header? ', Boolean(header)); - await this.removePanel(header); - } - - async customizePanel(parent?: WebElementWrapper) { - await this.openContextMenu(parent); - await testSubjects.click(CUSTOMIZE_PANEL_DATA_TEST_SUBJ); - } - - async replacePanelByTitle(title?: string) { - log.debug(`replacePanel(${title})`); - if (title) { - const panelOptions = await this.getPanelHeading(title); - await this.openContextMenu(panelOptions); - } else { - await this.openContextMenu(); - } - const actionExists = await testSubjects.exists(REPLACE_PANEL_DATA_TEST_SUBJ); - if (!actionExists) { - await this.clickContextMenuMoreItem(); - } - await testSubjects.click(REPLACE_PANEL_DATA_TEST_SUBJ); - } - - async clonePanelByTitle(title?: string) { - log.debug(`clonePanel(${title})`); - if (title) { - const panelOptions = await this.getPanelHeading(title); - await this.openContextMenu(panelOptions); - } else { - await this.openContextMenu(); - } - await testSubjects.click(CLONE_PANEL_DATA_TEST_SUBJ); - await PageObjects.dashboard.waitForRenderComplete(); - } - - async openCopyToModalByTitle(title?: string) { - log.debug(`copyPanelTo(${title})`); - if (title) { - const panelOptions = await this.getPanelHeading(title); - await this.openContextMenu(panelOptions); - } else { - await this.openContextMenu(); - } - const isActionVisible = await testSubjects.exists(COPY_PANEL_TO_DATA_TEST_SUBJ); - if (!isActionVisible) await this.clickContextMenuMoreItem(); - await testSubjects.click(COPY_PANEL_TO_DATA_TEST_SUBJ); - } - - async openInspectorByTitle(title: string) { - const header = await this.getPanelHeading(title); - await this.openInspector(header); - } - - async getSearchSessionIdByTitle(title: string) { - await this.openInspectorByTitle(title); - await inspector.openInspectorRequestsView(); - const searchSessionId = await ( - await testSubjects.find('inspectorRequestSearchSessionId') - ).getAttribute('data-search-session-id'); - await inspector.close(); - return searchSessionId; } - - async openInspector(parent?: WebElementWrapper) { - await this.openContextMenu(parent); - const exists = await testSubjects.exists(OPEN_INSPECTOR_TEST_SUBJ); - if (!exists) { - await this.clickContextMenuMoreItem(); - } - await testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); - } - - async unlinkFromLibary(parent?: WebElementWrapper) { - log.debug('unlinkFromLibrary'); - const libraryNotification = parent - ? await testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) - : await testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); - await libraryNotification.click(); - await testSubjects.click('libraryNotificationUnlinkButton'); - } - - async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { - log.debug('saveToLibrary'); - await this.openContextMenu(parent); - const exists = await testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); - if (!exists) { - await this.clickContextMenuMoreItem(); - } - await testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); - await testSubjects.setValue('savedObjectTitle', newTitle, { - clearWithKeyboard: true, - }); - await testSubjects.click('confirmSaveSavedObjectButton'); - } - - async expectExistsRemovePanelAction() { - log.debug('expectExistsRemovePanelAction'); - await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); + const actionExists = await this.testSubjects.exists(REPLACE_PANEL_DATA_TEST_SUBJ); + if (!actionExists) { + await this.clickContextMenuMoreItem(); } + await this.testSubjects.click(REPLACE_PANEL_DATA_TEST_SUBJ); + } - async expectExistsPanelAction(testSubject: string) { - log.debug('expectExistsPanelAction', testSubject); + async clonePanelByTitle(title?: string) { + this.log.debug(`clonePanel(${title})`); + if (title) { + const panelOptions = await this.getPanelHeading(title); + await this.openContextMenu(panelOptions); + } else { await this.openContextMenu(); - if (await testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return; - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); - } - await testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ); - await this.toggleContextMenu(); } + await this.testSubjects.click(CLONE_PANEL_DATA_TEST_SUBJ); + await this.PageObjects.dashboard.waitForRenderComplete(); + } - async expectMissingPanelAction(testSubject: string) { - log.debug('expectMissingPanelAction', testSubject); + async openCopyToModalByTitle(title?: string) { + this.log.debug(`copyPanelTo(${title})`); + if (title) { + const panelOptions = await this.getPanelHeading(title); + await this.openContextMenu(panelOptions); + } else { await this.openContextMenu(); - await testSubjects.missingOrFail(testSubject); - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); - await testSubjects.missingOrFail(testSubject); - } - await this.toggleContextMenu(); - } - - async expectExistsEditPanelAction() { - log.debug('expectExistsEditPanelAction'); - await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); - } - - async expectExistsReplacePanelAction() { - log.debug('expectExistsReplacePanelAction'); - await this.expectExistsPanelAction(REPLACE_PANEL_DATA_TEST_SUBJ); } - - async expectExistsClonePanelAction() { - log.debug('expectExistsClonePanelAction'); - await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ); - } - - async expectMissingEditPanelAction() { - log.debug('expectMissingEditPanelAction'); - await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); - } - - async expectMissingReplacePanelAction() { - log.debug('expectMissingReplacePanelAction'); - await this.expectMissingPanelAction(REPLACE_PANEL_DATA_TEST_SUBJ); - } - - async expectMissingDuplicatePanelAction() { - log.debug('expectMissingDuplicatePanelAction'); - await this.expectMissingPanelAction(CLONE_PANEL_DATA_TEST_SUBJ); - } - - async expectMissingRemovePanelAction() { - log.debug('expectMissingRemovePanelAction'); - await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); - } - - async expectExistsToggleExpandAction() { - log.debug('expectExistsToggleExpandAction'); - await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); - } - - async getPanelHeading(title: string) { - return await testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); + const isActionVisible = await this.testSubjects.exists(COPY_PANEL_TO_DATA_TEST_SUBJ); + if (!isActionVisible) await this.clickContextMenuMoreItem(); + await this.testSubjects.click(COPY_PANEL_TO_DATA_TEST_SUBJ); + } + + async openInspectorByTitle(title: string) { + const header = await this.getPanelHeading(title); + await this.openInspector(header); + } + + async getSearchSessionIdByTitle(title: string) { + await this.openInspectorByTitle(title); + await this.inspector.openInspectorRequestsView(); + const searchSessionId = await ( + await this.testSubjects.find('inspectorRequestSearchSessionId') + ).getAttribute('data-search-session-id'); + await this.inspector.close(); + return searchSessionId; + } + + async openInspector(parent?: WebElementWrapper) { + await this.openContextMenu(parent); + const exists = await this.testSubjects.exists(OPEN_INSPECTOR_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); } - - async clickHidePanelTitleToggle() { - await testSubjects.click('customizePanelHideTitle'); + await this.testSubjects.click(OPEN_INSPECTOR_TEST_SUBJ); + } + + async unlinkFromLibary(parent?: WebElementWrapper) { + this.log.debug('unlinkFromLibrary'); + const libraryNotification = parent + ? await this.testSubjects.findDescendant(LIBRARY_NOTIFICATION_TEST_SUBJ, parent) + : await this.testSubjects.find(LIBRARY_NOTIFICATION_TEST_SUBJ); + await libraryNotification.click(); + await this.testSubjects.click('libraryNotificationUnlinkButton'); + } + + async saveToLibrary(newTitle: string, parent?: WebElementWrapper) { + this.log.debug('saveToLibrary'); + await this.openContextMenu(parent); + const exists = await this.testSubjects.exists(SAVE_TO_LIBRARY_TEST_SUBJ); + if (!exists) { + await this.clickContextMenuMoreItem(); } - - async toggleHidePanelTitle(originalTitle?: string) { - log.debug(`hidePanelTitle(${originalTitle})`); - if (originalTitle) { - const panelOptions = await this.getPanelHeading(originalTitle); - await this.customizePanel(panelOptions); - } else { - await this.customizePanel(); - } - await this.clickHidePanelTitleToggle(); - await testSubjects.click('saveNewTitleButton'); + await this.testSubjects.click(SAVE_TO_LIBRARY_TEST_SUBJ); + await this.testSubjects.setValue('savedObjectTitle', newTitle, { + clearWithKeyboard: true, + }); + await this.testSubjects.click('confirmSaveSavedObjectButton'); + } + + async expectExistsRemovePanelAction() { + this.log.debug('expectExistsRemovePanelAction'); + await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); + } + + async expectExistsPanelAction(testSubject: string) { + this.log.debug('expectExistsPanelAction', testSubject); + await this.openContextMenu(); + if (await this.testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return; + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); } + await this.testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ); + await this.toggleContextMenu(); + } - /** - * - * @param customTitle - * @param originalTitle - optional to specify which panel to change the title on. - * @return {Promise} - */ - async setCustomPanelTitle(customTitle: string, originalTitle?: string) { - log.debug(`setCustomPanelTitle(${customTitle}, ${originalTitle})`); - if (originalTitle) { - const panelOptions = await this.getPanelHeading(originalTitle); - await this.customizePanel(panelOptions); - } else { - await this.customizePanel(); + async expectMissingPanelAction(testSubject: string) { + this.log.debug('expectMissingPanelAction', testSubject); + await this.openContextMenu(); + await this.testSubjects.missingOrFail(testSubject); + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); + await this.testSubjects.missingOrFail(testSubject); + } + await this.toggleContextMenu(); + } + + async expectExistsEditPanelAction() { + this.log.debug('expectExistsEditPanelAction'); + await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); + } + + async expectExistsReplacePanelAction() { + this.log.debug('expectExistsReplacePanelAction'); + await this.expectExistsPanelAction(REPLACE_PANEL_DATA_TEST_SUBJ); + } + + async expectExistsClonePanelAction() { + this.log.debug('expectExistsClonePanelAction'); + await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingEditPanelAction() { + this.log.debug('expectMissingEditPanelAction'); + await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingReplacePanelAction() { + this.log.debug('expectMissingReplacePanelAction'); + await this.expectMissingPanelAction(REPLACE_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingDuplicatePanelAction() { + this.log.debug('expectMissingDuplicatePanelAction'); + await this.expectMissingPanelAction(CLONE_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingRemovePanelAction() { + this.log.debug('expectMissingRemovePanelAction'); + await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); + } + + async expectExistsToggleExpandAction() { + this.log.debug('expectExistsToggleExpandAction'); + await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + } + + async getPanelHeading(title: string) { + return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); + } + + async clickHidePanelTitleToggle() { + await this.testSubjects.click('customizePanelHideTitle'); + } + + async toggleHidePanelTitle(originalTitle?: string) { + this.log.debug(`hidePanelTitle(${originalTitle})`); + if (originalTitle) { + const panelOptions = await this.getPanelHeading(originalTitle); + await this.customizePanel(panelOptions); + } else { + await this.customizePanel(); + } + await this.clickHidePanelTitleToggle(); + await this.testSubjects.click('saveNewTitleButton'); + } + + /** + * + * @param customTitle + * @param originalTitle - optional to specify which panel to change the title on. + * @return {Promise} + */ + async setCustomPanelTitle(customTitle: string, originalTitle?: string) { + this.log.debug(`setCustomPanelTitle(${customTitle}, ${originalTitle})`); + if (originalTitle) { + const panelOptions = await this.getPanelHeading(originalTitle); + await this.customizePanel(panelOptions); + } else { + await this.customizePanel(); + } + await this.testSubjects.setValue('customEmbeddablePanelTitleInput', customTitle); + await this.testSubjects.click('saveNewTitleButton'); + } + + async resetCustomPanelTitle(panel?: WebElementWrapper) { + this.log.debug('resetCustomPanelTitle'); + await this.customizePanel(panel); + await this.testSubjects.click('resetCustomEmbeddablePanelTitle'); + await this.testSubjects.click('saveNewTitleButton'); + await this.toggleContextMenu(panel); + } + + async getActionWebElementByText(text: string): Promise { + this.log.debug(`getActionWebElement: "${text}"`); + const menu = await this.testSubjects.find('multipleActionsContextMenu'); + const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); + for (const item of items) { + const currentText = await item.getVisibleText(); + if (currentText === text) { + return item; } - await testSubjects.setValue('customEmbeddablePanelTitleInput', customTitle); - await testSubjects.click('saveNewTitleButton'); } - async resetCustomPanelTitle(panel?: WebElementWrapper) { - log.debug('resetCustomPanelTitle'); - await this.customizePanel(panel); - await testSubjects.click('resetCustomEmbeddablePanelTitle'); - await testSubjects.click('saveNewTitleButton'); - await this.toggleContextMenu(panel); - } - - async getActionWebElementByText(text: string): Promise { - log.debug(`getActionWebElement: "${text}"`); - const menu = await testSubjects.find('multipleActionsContextMenu'); - const items = await menu.findAllByCssSelector('[data-test-subj*="embeddablePanelAction-"]'); - for (const item of items) { - const currentText = await item.getVisibleText(); - if (currentText === text) { - return item; - } - } - - throw new Error(`No action matching text "${text}"`); - } - })(); + throw new Error(`No action matching text "${text}"`); + } } diff --git a/test/functional/services/dashboard/replace_panel.ts b/test/functional/services/dashboard/replace_panel.ts index 3b33b5cd4f6a65..8f8f680b839bf3 100644 --- a/test/functional/services/dashboard/replace_panel.ts +++ b/test/functional/services/dashboard/replace_panel.ts @@ -6,89 +6,87 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; -export function DashboardReplacePanelProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const testSubjects = getService('testSubjects'); - const flyout = getService('flyout'); +export class DashboardReplacePanelService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly flyout = this.ctx.getService('flyout'); - return new (class DashboardReplacePanel { - async toggleFilterPopover() { - log.debug('DashboardReplacePanel.toggleFilter'); - await testSubjects.click('savedObjectFinderFilterButton'); - } + async toggleFilterPopover() { + this.log.debug('DashboardReplacePanel.toggleFilter'); + await this.testSubjects.click('savedObjectFinderFilterButton'); + } - async toggleFilter(type: string) { - log.debug(`DashboardReplacePanel.replaceToFilter(${type})`); - await this.waitForListLoading(); - await this.toggleFilterPopover(); - await testSubjects.click(`savedObjectFinderFilter-${type}`); - await this.toggleFilterPopover(); - } + async toggleFilter(type: string) { + this.log.debug(`DashboardReplacePanel.replaceToFilter(${type})`); + await this.waitForListLoading(); + await this.toggleFilterPopover(); + await this.testSubjects.click(`savedObjectFinderFilter-${type}`); + await this.toggleFilterPopover(); + } - async isReplacePanelOpen() { - log.debug('DashboardReplacePanel.isReplacePanelOpen'); - return await testSubjects.exists('dashboardReplacePanel'); - } + async isReplacePanelOpen() { + this.log.debug('DashboardReplacePanel.isReplacePanelOpen'); + return await this.testSubjects.exists('dashboardReplacePanel'); + } - async ensureReplacePanelIsShowing() { - log.debug('DashboardReplacePanel.ensureReplacePanelIsShowing'); - const isOpen = await this.isReplacePanelOpen(); - if (!isOpen) { - throw new Error('Replace panel is not open, trying again.'); - } + async ensureReplacePanelIsShowing() { + this.log.debug('DashboardReplacePanel.ensureReplacePanelIsShowing'); + const isOpen = await this.isReplacePanelOpen(); + if (!isOpen) { + throw new Error('Replace panel is not open, trying again.'); } + } - async waitForListLoading() { - await testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator'); - } + async waitForListLoading() { + await this.testSubjects.waitForDeleted('savedObjectFinderLoadingIndicator'); + } - async closeReplacePanel() { - await flyout.ensureClosed('dashboardReplacePanel'); - } + async closeReplacePanel() { + await this.flyout.ensureClosed('dashboardReplacePanel'); + } - async replaceSavedSearch(searchName: string) { - return this.replaceEmbeddable(searchName, 'search'); - } + async replaceSavedSearch(searchName: string) { + return this.replaceEmbeddable(searchName, 'search'); + } - async replaceSavedSearches(searches: string[]) { - for (const name of searches) { - await this.replaceSavedSearch(name); - } + async replaceSavedSearches(searches: string[]) { + for (const name of searches) { + await this.replaceSavedSearch(name); } + } - async replaceVisualization(vizName: string) { - return this.replaceEmbeddable(vizName, 'visualization'); - } + async replaceVisualization(vizName: string) { + return this.replaceEmbeddable(vizName, 'visualization'); + } - async replaceEmbeddable(embeddableName: string, embeddableType?: string) { - log.debug( - `DashboardReplacePanel.replaceEmbeddable, name: ${embeddableName}, type: ${embeddableType}` - ); - await this.ensureReplacePanelIsShowing(); - if (embeddableType) { - await this.toggleFilter(embeddableType); - } - await this.filterEmbeddableNames(`"${embeddableName.replace('-', ' ')}"`); - await testSubjects.click(`savedObjectTitle${embeddableName.split(' ').join('-')}`); - await testSubjects.exists('addObjectToDashboardSuccess'); - await this.closeReplacePanel(); - return embeddableName; + async replaceEmbeddable(embeddableName: string, embeddableType?: string) { + this.log.debug( + `DashboardReplacePanel.replaceEmbeddable, name: ${embeddableName}, type: ${embeddableType}` + ); + await this.ensureReplacePanelIsShowing(); + if (embeddableType) { + await this.toggleFilter(embeddableType); } + await this.filterEmbeddableNames(`"${embeddableName.replace('-', ' ')}"`); + await this.testSubjects.click(`savedObjectTitle${embeddableName.split(' ').join('-')}`); + await this.testSubjects.exists('addObjectToDashboardSuccess'); + await this.closeReplacePanel(); + return embeddableName; + } - async filterEmbeddableNames(name: string) { - // The search input field may be disabled while the table is loading so wait for it - await this.waitForListLoading(); - await testSubjects.setValue('savedObjectFinderSearchInput', name); - await this.waitForListLoading(); - } + async filterEmbeddableNames(name: string) { + // The search input field may be disabled while the table is loading so wait for it + await this.waitForListLoading(); + await this.testSubjects.setValue('savedObjectFinderSearchInput', name); + await this.waitForListLoading(); + } - async panelReplaceLinkExists(name: string) { - log.debug(`DashboardReplacePanel.panelReplaceLinkExists(${name})`); - await this.ensureReplacePanelIsShowing(); - await this.filterEmbeddableNames(`"${name}"`); - return await testSubjects.exists(`savedObjectTitle${name.split(' ').join('-')}`); - } - })(); + async panelReplaceLinkExists(name: string) { + this.log.debug(`DashboardReplacePanel.panelReplaceLinkExists(${name})`); + await this.ensureReplacePanelIsShowing(); + await this.filterEmbeddableNames(`"${name}"`); + return await this.testSubjects.exists(`savedObjectTitle${name.split(' ').join('-')}`); + } } diff --git a/test/functional/services/dashboard/visualizations.ts b/test/functional/services/dashboard/visualizations.ts index 2bf7458ff9c5f4..a6b88802d7b814 100644 --- a/test/functional/services/dashboard/visualizations.ts +++ b/test/functional/services/dashboard/visualizations.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; -export function DashboardVisualizationProvider({ getService, getPageObjects }: FtrProviderContext) { - const log = getService('log'); - const queryBar = getService('queryBar'); - const testSubjects = getService('testSubjects'); - const dashboardAddPanel = getService('dashboardAddPanel'); - const PageObjects = getPageObjects([ +export class DashboardVisualizationsService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly queryBar = this.ctx.getService('queryBar'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly dashboardAddPanel = this.ctx.getService('dashboardAddPanel'); + private readonly PageObjects = this.ctx.getPageObjects([ 'dashboard', 'visualize', 'visEditor', @@ -22,108 +22,106 @@ export function DashboardVisualizationProvider({ getService, getPageObjects }: F 'timePicker', ]); - return new (class DashboardVisualizations { - async createAndAddTSVBVisualization(name: string) { - log.debug(`createAndAddTSVBVisualization(${name})`); - const inViewMode = await PageObjects.dashboard.getIsInViewMode(); - if (inViewMode) { - await PageObjects.dashboard.switchToEditMode(); - } - await dashboardAddPanel.clickEditorMenuButton(); - await dashboardAddPanel.clickAddNewEmbeddableLink('metrics'); - await PageObjects.visualize.clickVisualBuilder(); - await PageObjects.visualize.saveVisualizationExpectSuccess(name); + async createAndAddTSVBVisualization(name: string) { + this.log.debug(`createAndAddTSVBVisualization(${name})`); + const inViewMode = await this.PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await this.PageObjects.dashboard.switchToEditMode(); } + await this.dashboardAddPanel.clickEditorMenuButton(); + await this.dashboardAddPanel.clickAddNewEmbeddableLink('metrics'); + await this.PageObjects.visualize.clickVisualBuilder(); + await this.PageObjects.visualize.saveVisualizationExpectSuccess(name); + } - async createSavedSearch({ - name, - query, - fields, - }: { - name: string; - query?: string; - fields?: string[]; - }) { - log.debug(`createSavedSearch(${name})`); - await PageObjects.header.clickDiscover(true); - await PageObjects.timePicker.setHistoricalDataRange(); + async createSavedSearch({ + name, + query, + fields, + }: { + name: string; + query?: string; + fields?: string[]; + }) { + this.log.debug(`createSavedSearch(${name})`); + await this.PageObjects.header.clickDiscover(true); + await this.PageObjects.timePicker.setHistoricalDataRange(); - if (query) { - await queryBar.setQuery(query); - await queryBar.submitQuery(); - } + if (query) { + await this.queryBar.setQuery(query); + await this.queryBar.submitQuery(); + } - if (fields) { - for (let i = 0; i < fields.length; i++) { - await PageObjects.discover.clickFieldListItemAdd(fields[i]); - } + if (fields) { + for (let i = 0; i < fields.length; i++) { + await this.PageObjects.discover.clickFieldListItemAdd(fields[i]); } - - await PageObjects.discover.saveSearch(name); - await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.exists('saveSearchSuccess'); } - async createAndAddSavedSearch({ - name, - query, - fields, - }: { - name: string; - query?: string; - fields?: string[]; - }) { - log.debug(`createAndAddSavedSearch(${name})`); - await this.createSavedSearch({ name, query, fields }); + await this.PageObjects.discover.saveSearch(name); + await this.PageObjects.header.waitUntilLoadingHasFinished(); + await this.testSubjects.exists('saveSearchSuccess'); + } - await PageObjects.header.clickDashboard(); + async createAndAddSavedSearch({ + name, + query, + fields, + }: { + name: string; + query?: string; + fields?: string[]; + }) { + this.log.debug(`createAndAddSavedSearch(${name})`); + await this.createSavedSearch({ name, query, fields }); - const inViewMode = await PageObjects.dashboard.getIsInViewMode(); - if (inViewMode) { - await PageObjects.dashboard.switchToEditMode(); - } - await dashboardAddPanel.addSavedSearch(name); + await this.PageObjects.header.clickDashboard(); + + const inViewMode = await this.PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await this.PageObjects.dashboard.switchToEditMode(); } + await this.dashboardAddPanel.addSavedSearch(name); + } - async createAndAddMarkdown({ name, markdown }: { name: string; markdown: string }) { - log.debug(`createAndAddMarkdown(${markdown})`); - const inViewMode = await PageObjects.dashboard.getIsInViewMode(); - if (inViewMode) { - await PageObjects.dashboard.switchToEditMode(); - } - await dashboardAddPanel.clickMarkdownQuickButton(); - await PageObjects.visEditor.setMarkdownTxt(markdown); - await PageObjects.visEditor.clickGo(); - await PageObjects.visualize.saveVisualizationExpectSuccess(name, { - saveAsNew: false, - redirectToOrigin: true, - }); + async createAndAddMarkdown({ name, markdown }: { name: string; markdown: string }) { + this.log.debug(`createAndAddMarkdown(${markdown})`); + const inViewMode = await this.PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await this.PageObjects.dashboard.switchToEditMode(); } + await this.dashboardAddPanel.clickMarkdownQuickButton(); + await this.PageObjects.visEditor.setMarkdownTxt(markdown); + await this.PageObjects.visEditor.clickGo(); + await this.PageObjects.visualize.saveVisualizationExpectSuccess(name, { + saveAsNew: false, + redirectToOrigin: true, + }); + } - async createAndEmbedMetric(name: string) { - log.debug(`createAndEmbedMetric(${name})`); - const inViewMode = await PageObjects.dashboard.getIsInViewMode(); - if (inViewMode) { - await PageObjects.dashboard.switchToEditMode(); - } - await dashboardAddPanel.clickEditorMenuButton(); - await dashboardAddPanel.clickAggBasedVisualizations(); - await dashboardAddPanel.clickVisType('metric'); - await testSubjects.click('savedObjectTitlelogstash-*'); - await testSubjects.exists('visualizesaveAndReturnButton'); - await testSubjects.click('visualizesaveAndReturnButton'); + async createAndEmbedMetric(name: string) { + this.log.debug(`createAndEmbedMetric(${name})`); + const inViewMode = await this.PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await this.PageObjects.dashboard.switchToEditMode(); } + await this.dashboardAddPanel.clickEditorMenuButton(); + await this.dashboardAddPanel.clickAggBasedVisualizations(); + await this.dashboardAddPanel.clickVisType('metric'); + await this.testSubjects.click('savedObjectTitlelogstash-*'); + await this.testSubjects.exists('visualizesaveAndReturnButton'); + await this.testSubjects.click('visualizesaveAndReturnButton'); + } - async createAndEmbedMarkdown({ name, markdown }: { name: string; markdown: string }) { - log.debug(`createAndEmbedMarkdown(${markdown})`); - const inViewMode = await PageObjects.dashboard.getIsInViewMode(); - if (inViewMode) { - await PageObjects.dashboard.switchToEditMode(); - } - await dashboardAddPanel.clickMarkdownQuickButton(); - await PageObjects.visEditor.setMarkdownTxt(markdown); - await PageObjects.visEditor.clickGo(); - await testSubjects.click('visualizesaveAndReturnButton'); + async createAndEmbedMarkdown({ name, markdown }: { name: string; markdown: string }) { + this.log.debug(`createAndEmbedMarkdown(${markdown})`); + const inViewMode = await this.PageObjects.dashboard.getIsInViewMode(); + if (inViewMode) { + await this.PageObjects.dashboard.switchToEditMode(); } - })(); + await this.dashboardAddPanel.clickMarkdownQuickButton(); + await this.PageObjects.visEditor.setMarkdownTxt(markdown); + await this.PageObjects.visEditor.clickGo(); + await this.testSubjects.click('visualizesaveAndReturnButton'); + } } diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index ee50185db8c688..a00587c9789778 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -7,7 +7,7 @@ */ import { chunk } from 'lodash'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; import { WebElementWrapper } from './lib/web_element_wrapper'; interface TabbedGridData { @@ -19,261 +19,254 @@ interface SelectOptions { rowIndex: number; } -export function DataGridProvider({ getService, getPageObjects }: FtrProviderContext) { - const find = getService('find'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'header']); - const retry = getService('retry'); - - class DataGrid { - async getDataGridTableData(): Promise { - const table = await find.byCssSelector('.euiDataGrid'); - const $ = await table.parseDomContent(); - - const columns = $('.euiDataGridHeaderCell__content') - .toArray() - .map((cell) => $(cell).text()); - const cells = $.findTestSubjects('dataGridRowCell') - .toArray() - .map((cell) => $(cell).text()); - - const rows = chunk(cells, columns.length); - - return { - columns, - rows, - }; - } - - /** - * Converts the data grid data into nested array - * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] - * @param element table - */ - public async getDataFromElement( - element: WebElementWrapper, - cellDataTestSubj: string - ): Promise { - const $ = await element.parseDomContent(); - const columnNumber = $('.euiDataGridHeaderCell__content').length; - const cells = $.findTestSubjects('dataGridRowCell') - .toArray() - .map((cell) => - $(cell) - .findTestSubject(cellDataTestSubj) - .text() - .replace(/ /g, '') - .trim() - ); - - return chunk(cells, columnNumber); - } - - /** - * Returns an array of data grid headers names - */ - public async getHeaders() { - const header = await testSubjects.find('dataGridWrapper > dataGridHeader'); - const $ = await header.parseDomContent(); - return $('.euiDataGridHeaderCell__content') - .toArray() - .map((cell) => $(cell).text()); - } +export class DataGridService extends FtrService { + private readonly find = this.ctx.getService('find'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly PageObjects = this.ctx.getPageObjects(['common', 'header']); + private readonly retry = this.ctx.getService('retry'); + + async getDataGridTableData(): Promise { + const table = await this.find.byCssSelector('.euiDataGrid'); + const $ = await table.parseDomContent(); + + const columns = $('.euiDataGridHeaderCell__content') + .toArray() + .map((cell) => $(cell).text()); + const cells = $.findTestSubjects('dataGridRowCell') + .toArray() + .map((cell) => $(cell).text()); + + const rows = chunk(cells, columns.length); + + return { + columns, + rows, + }; + } - /** - * Returns a grid cell element by row & column indexes. - * The row offset equals 1 since the first row of data grid is the header row. - * @param rowIndex data row index starting from 1 (1 means 1st row) - * @param columnIndex column index starting from 1 (1 means 1st column) - */ - public async getCellElement(rowIndex: number, columnIndex: number) { - const table = await find.byCssSelector('.euiDataGrid'); - const $ = await table.parseDomContent(); - const columnNumber = $('.euiDataGridHeaderCell__content').length; - return await find.byCssSelector( - `[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRowCell"]:nth-of-type(${ - columnNumber * (rowIndex - 1) + columnIndex + 1 - })` + /** + * Converts the data grid data into nested array + * [ [cell1_in_row1, cell2_in_row1], [cell1_in_row2, cell2_in_row2] ] + * @param element table + */ + public async getDataFromElement( + element: WebElementWrapper, + cellDataTestSubj: string + ): Promise { + const $ = await element.parseDomContent(); + const columnNumber = $('.euiDataGridHeaderCell__content').length; + const cells = $.findTestSubjects('dataGridRowCell') + .toArray() + .map((cell) => + $(cell) + .findTestSubject(cellDataTestSubj) + .text() + .replace(/ /g, '') + .trim() ); - } - public async getDocCount(): Promise { - const grid = await find.byCssSelector('[data-document-number]'); - return Number(await grid.getAttribute('data-document-number')); - } + return chunk(cells, columnNumber); + } - public async getFields() { - const cells = await find.allByCssSelector('.euiDataGridRowCell'); - - const rows: string[][] = []; - let rowIdx = -1; - for (const cell of cells) { - if (await cell.elementHasClass('euiDataGridRowCell--firstColumn')) { - // first column contains expand icon - rowIdx++; - rows[rowIdx] = []; - } - if (!(await cell.elementHasClass('euiDataGridRowCell--controlColumn'))) { - rows[rowIdx].push(await cell.getVisibleText()); - } - } - return rows; - } + /** + * Returns an array of data grid headers names + */ + public async getHeaders() { + const header = await this.testSubjects.find('dataGridWrapper > dataGridHeader'); + const $ = await header.parseDomContent(); + return $('.euiDataGridHeaderCell__content') + .toArray() + .map((cell) => $(cell).text()); + } - public async getTable(selector: string = 'docTable') { - return await testSubjects.find(selector); - } + /** + * Returns a grid cell element by row & column indexes. + * The row offset equals 1 since the first row of data grid is the header row. + * @param rowIndex data row index starting from 1 (1 means 1st row) + * @param columnIndex column index starting from 1 (1 means 1st column) + */ + public async getCellElement(rowIndex: number, columnIndex: number) { + const table = await this.find.byCssSelector('.euiDataGrid'); + const $ = await table.parseDomContent(); + const columnNumber = $('.euiDataGridHeaderCell__content').length; + return await this.find.byCssSelector( + `[data-test-subj="dataGridWrapper"] [data-test-subj="dataGridRowCell"]:nth-of-type(${ + columnNumber * (rowIndex - 1) + columnIndex + 1 + })` + ); + } - public async getBodyRows(): Promise { - return this.getDocTableRows(); - } + public async getDocCount(): Promise { + const grid = await this.find.byCssSelector('[data-document-number]'); + return Number(await grid.getAttribute('data-document-number')); + } - /** - * Returns an array of rows (which are array of cells) - */ - public async getDocTableRows() { - const table = await this.getTable(); - if (!table) { - return []; + public async getFields() { + const cells = await this.find.allByCssSelector('.euiDataGridRowCell'); + + const rows: string[][] = []; + let rowIdx = -1; + for (const cell of cells) { + if (await cell.elementHasClass('euiDataGridRowCell--firstColumn')) { + // first column contains expand icon + rowIdx++; + rows[rowIdx] = []; } - const cells = await table.findAllByCssSelector('.euiDataGridRowCell'); - - const rows: WebElementWrapper[][] = []; - let rowIdx = -1; - for (const cell of cells) { - if (await cell.elementHasClass('euiDataGridRowCell--firstColumn')) { - rowIdx++; - rows[rowIdx] = []; - } - rows[rowIdx].push(cell); + if (!(await cell.elementHasClass('euiDataGridRowCell--controlColumn'))) { + rows[rowIdx].push(await cell.getVisibleText()); } - return rows; - } - - /** - * Returns an array of cells for that row - */ - public async getRow(options: SelectOptions): Promise { - return (await this.getBodyRows())[options.rowIndex]; } + return rows; + } - public async clickRowToggle( - options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } - ): Promise { - const row = await this.getRow(options); - const toggle = await row[0]; - await toggle.click(); - } + public async getTable(selector: string = 'docTable') { + return await this.testSubjects.find(selector); + } - public async getDetailsRows(): Promise { - return await testSubjects.findAll('docTableDetailsFlyout'); - } + public async getBodyRows(): Promise { + return this.getDocTableRows(); + } - public async closeFlyout() { - await testSubjects.click('euiFlyoutCloseButton'); + /** + * Returns an array of rows (which are array of cells) + */ + public async getDocTableRows() { + const table = await this.getTable(); + if (!table) { + return []; } - - public async getHeaderFields(): Promise { - const result = await find.allByCssSelector('.euiDataGridHeaderCell__content'); - const textArr = []; - let idx = 0; - for (const cell of result) { - if (idx > 1) { - textArr.push(await cell.getVisibleText()); - } - idx++; + const cells = await table.findAllByCssSelector('.euiDataGridRowCell'); + + const rows: WebElementWrapper[][] = []; + let rowIdx = -1; + for (const cell of cells) { + if (await cell.elementHasClass('euiDataGridRowCell--firstColumn')) { + rowIdx++; + rows[rowIdx] = []; } - return Promise.resolve(textArr); + rows[rowIdx].push(cell); } + return rows; + } - public async getRowActions( - options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } - ): Promise { - const detailsRow = (await this.getDetailsRows())[options.rowIndex]; - return await detailsRow.findAllByTestSubject('~docTableRowAction'); - } + /** + * Returns an array of cells for that row + */ + public async getRow(options: SelectOptions): Promise { + return (await this.getBodyRows())[options.rowIndex]; + } - public async openColMenuByField(field: string) { - await retry.waitFor('header cell action being displayed', async () => { - // to prevent flakiness - await testSubjects.click(`dataGridHeaderCell-${field}`); - return await testSubjects.exists(`dataGridHeaderCellActionGroup-${field}`); - }); - } + public async clickRowToggle( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const row = await this.getRow(options); + const toggle = await row[0]; + await toggle.click(); + } - public async clickDocSortAsc(field?: string, sortText = 'Sort New-Old') { - if (field) { - await this.openColMenuByField(field); - } else { - await find.clickByCssSelector('.euiDataGridHeaderCell__button'); - } - await find.clickByButtonText(sortText); - } + public async getDetailsRows(): Promise { + return await this.testSubjects.findAll('docTableDetailsFlyout'); + } - public async clickDocSortDesc(field?: string, sortText = 'Sort Old-New') { - if (field) { - await this.openColMenuByField(field); - } else { - await find.clickByCssSelector('.euiDataGridHeaderCell__button'); - } - await find.clickByButtonText(sortText); - } + public async closeFlyout() { + await this.testSubjects.click('euiFlyoutCloseButton'); + } - public async clickRemoveColumn(field?: string) { - if (field) { - await this.openColMenuByField(field); - } else { - await find.clickByCssSelector('.euiDataGridHeaderCell__button'); + public async getHeaderFields(): Promise { + const result = await this.find.allByCssSelector('.euiDataGridHeaderCell__content'); + const textArr = []; + let idx = 0; + for (const cell of result) { + if (idx > 1) { + textArr.push(await cell.getVisibleText()); } - await find.clickByButtonText('Remove column'); - } - public async getDetailsRow(): Promise { - const detailRows = await this.getDetailsRows(); - return detailRows[0]; - } - public async addInclusiveFilter( - detailsRow: WebElementWrapper, - fieldName: string - ): Promise { - const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); - const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); - await addInclusiveFilterButton.click(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + idx++; } + return Promise.resolve(textArr); + } - public async getAddInclusiveFilterButton( - tableDocViewRow: WebElementWrapper - ): Promise { - return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`); - } + public async getRowActions( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const detailsRow = (await this.getDetailsRows())[options.rowIndex]; + return await detailsRow.findAllByTestSubject('~docTableRowAction'); + } - public async getTableDocViewRow( - detailsRow: WebElementWrapper, - fieldName: string - ): Promise { - return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`); - } + public async openColMenuByField(field: string) { + await this.retry.waitFor('header cell action being displayed', async () => { + // to prevent flakiness + await this.testSubjects.click(`dataGridHeaderCell-${field}`); + return await this.testSubjects.exists(`dataGridHeaderCellActionGroup-${field}`); + }); + } - public async getRemoveInclusiveFilterButton( - tableDocViewRow: WebElementWrapper - ): Promise { - return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); + public async clickDocSortAsc(field?: string, sortText = 'Sort New-Old') { + if (field) { + await this.openColMenuByField(field); + } else { + await this.find.clickByCssSelector('.euiDataGridHeaderCell__button'); } + await this.find.clickByButtonText(sortText); + } - public async removeInclusiveFilter( - detailsRow: WebElementWrapper, - fieldName: string - ): Promise { - const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); - const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow); - await addInclusiveFilterButton.click(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + public async clickDocSortDesc(field?: string, sortText = 'Sort Old-New') { + if (field) { + await this.openColMenuByField(field); + } else { + await this.find.clickByCssSelector('.euiDataGridHeaderCell__button'); } + await this.find.clickByButtonText(sortText); + } - public async hasNoResults() { - return await find.existsByCssSelector('.euiDataGrid__noResults'); + public async clickRemoveColumn(field?: string) { + if (field) { + await this.openColMenuByField(field); + } else { + await this.find.clickByCssSelector('.euiDataGridHeaderCell__button'); } + await this.find.clickByButtonText('Remove column'); + } + public async getDetailsRow(): Promise { + const detailRows = await this.getDetailsRows(); + return detailRows[0]; + } + public async addInclusiveFilter(detailsRow: WebElementWrapper, fieldName: string): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async getAddInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`); } - return new DataGrid(); + public async getTableDocViewRow( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`); + } + + public async getRemoveInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); + } + + public async removeInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async hasNoResults() { + return await this.find.existsByCssSelector('.euiDataGrid__noResults'); + } } diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts index 35c3531c70c417..6c73faec16b1a9 100644 --- a/test/functional/services/doc_table.ts +++ b/test/functional/services/doc_table.ts @@ -6,177 +6,170 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; import { WebElementWrapper } from './lib/web_element_wrapper'; -export function DocTableProvider({ getService, getPageObjects }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const PageObjects = getPageObjects(['common', 'header']); +interface SelectOptions { + isAnchorRow?: boolean; + rowIndex?: number; +} + +export class DocTableService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly retry = this.ctx.getService('retry'); + private readonly PageObjects = this.ctx.getPageObjects(['common', 'header']); - interface SelectOptions { - isAnchorRow?: boolean; - rowIndex?: number; + public async getTable(selector?: string) { + return await this.testSubjects.find(selector ? selector : 'docTable'); } - class DocTable { - public async getTable(selector?: string) { - return await testSubjects.find(selector ? selector : 'docTable'); - } + public async getRowsText() { + const table = await this.getTable(); + const $ = await table.parseDomContent(); + return $.findTestSubjects('~docTableRow') + .toArray() + .map((row: any) => $(row).text().trim()); + } - public async getRowsText() { - const table = await this.getTable(); - const $ = await table.parseDomContent(); - return $.findTestSubjects('~docTableRow') - .toArray() - .map((row: any) => $(row).text().trim()); - } - - public async getBodyRows(): Promise { - const table = await this.getTable(); - return await table.findAllByTestSubject('~docTableRow'); - } - - public async getAnchorRow(): Promise { - const table = await this.getTable(); - return await table.findByTestSubject('~docTableAnchorRow'); - } - - public async getRow({ - isAnchorRow = false, - rowIndex = 0, - }: SelectOptions = {}): Promise { - return isAnchorRow ? await this.getAnchorRow() : (await this.getBodyRows())[rowIndex]; - } - - public async getDetailsRow(): Promise { - const table = await this.getTable(); - return await table.findByCssSelector('[data-test-subj~="docTableDetailsRow"]'); - } - - public async getAnchorDetailsRow(): Promise { - const table = await this.getTable(); - return await table.findByCssSelector( - '[data-test-subj~="docTableAnchorRow"] + [data-test-subj~="docTableDetailsRow"]' - ); - } - - public async clickRowToggle( - options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } - ): Promise { - const row = await this.getRow(options); - const toggle = await row.findByTestSubject('~docTableExpandToggleColumn'); - await toggle.click(); - } - - public async getDetailsRows(): Promise { - const table = await this.getTable(); - return await table.findAllByCssSelector( - '[data-test-subj~="docTableRow"] + [data-test-subj~="docTableDetailsRow"]' - ); - } - - public async getRowActions({ isAnchorRow = false, rowIndex = 0 }: SelectOptions = {}): Promise< - WebElementWrapper[] - > { - const detailsRow = isAnchorRow - ? await this.getAnchorDetailsRow() - : (await this.getDetailsRows())[rowIndex]; - return await detailsRow.findAllByTestSubject('~docTableRowAction'); - } - - public async getFields(options: { isAnchorRow: boolean } = { isAnchorRow: false }) { - const table = await this.getTable(); - const $ = await table.parseDomContent(); - const rowLocator = options.isAnchorRow ? '~docTableAnchorRow' : '~docTableRow'; - const rows = $.findTestSubjects(rowLocator).toArray(); - return rows.map((row: any) => - $(row) - .find('[data-test-subj~="docTableField"]') - .toArray() - .map((field: any) => $(field).text()) - ); - } + public async getBodyRows(): Promise { + const table = await this.getTable(); + return await table.findAllByTestSubject('~docTableRow'); + } + + public async getAnchorRow(): Promise { + const table = await this.getTable(); + return await table.findByTestSubject('~docTableAnchorRow'); + } + + public async getRow({ + isAnchorRow = false, + rowIndex = 0, + }: SelectOptions = {}): Promise { + return isAnchorRow ? await this.getAnchorRow() : (await this.getBodyRows())[rowIndex]; + } + + public async getDetailsRow(): Promise { + const table = await this.getTable(); + return await table.findByCssSelector('[data-test-subj~="docTableDetailsRow"]'); + } + + public async getAnchorDetailsRow(): Promise { + const table = await this.getTable(); + return await table.findByCssSelector( + '[data-test-subj~="docTableAnchorRow"] + [data-test-subj~="docTableDetailsRow"]' + ); + } + + public async clickRowToggle( + options: SelectOptions = { isAnchorRow: false, rowIndex: 0 } + ): Promise { + const row = await this.getRow(options); + const toggle = await row.findByTestSubject('~docTableExpandToggleColumn'); + await toggle.click(); + } + + public async getDetailsRows(): Promise { + const table = await this.getTable(); + return await table.findAllByCssSelector( + '[data-test-subj~="docTableRow"] + [data-test-subj~="docTableDetailsRow"]' + ); + } + + public async getRowActions({ isAnchorRow = false, rowIndex = 0 }: SelectOptions = {}): Promise< + WebElementWrapper[] + > { + const detailsRow = isAnchorRow + ? await this.getAnchorDetailsRow() + : (await this.getDetailsRows())[rowIndex]; + return await detailsRow.findAllByTestSubject('~docTableRowAction'); + } - public async getHeaderFields(selector?: string): Promise { - const table = await this.getTable(selector); - const $ = await table.parseDomContent(); - return $.findTestSubjects('~docTableHeaderField') + public async getFields(options: { isAnchorRow: boolean } = { isAnchorRow: false }) { + const table = await this.getTable(); + const $ = await table.parseDomContent(); + const rowLocator = options.isAnchorRow ? '~docTableAnchorRow' : '~docTableRow'; + const rows = $.findTestSubjects(rowLocator).toArray(); + return rows.map((row: any) => + $(row) + .find('[data-test-subj~="docTableField"]') .toArray() - .map((field: any) => $(field).text().trim()); - } - - public async getHeaders(selector?: string): Promise { - return this.getHeaderFields(selector); - } - - public async getTableDocViewRow( - detailsRow: WebElementWrapper, - fieldName: string - ): Promise { - return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`); - } - - public async getAddInclusiveFilterButton( - tableDocViewRow: WebElementWrapper - ): Promise { - return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`); - } - - public async addInclusiveFilter( - detailsRow: WebElementWrapper, - fieldName: string - ): Promise { - const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); - const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); - await addInclusiveFilterButton.click(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } - - public async getRemoveInclusiveFilterButton( - tableDocViewRow: WebElementWrapper - ): Promise { - return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); - } - - public async removeInclusiveFilter( - detailsRow: WebElementWrapper, - fieldName: string - ): Promise { - const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); - const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow); - await addInclusiveFilterButton.click(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } - - public async getAddExistsFilterButton( - tableDocViewRow: WebElementWrapper - ): Promise { - return await tableDocViewRow.findByTestSubject(`~addExistsFilterButton`); - } - - public async addExistsFilter(detailsRow: WebElementWrapper, fieldName: string): Promise { - const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); - const addInclusiveFilterButton = await this.getAddExistsFilterButton(tableDocViewRow); - await addInclusiveFilterButton.click(); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } - - public async toggleRowExpanded({ - isAnchorRow = false, - rowIndex = 0, - }: SelectOptions = {}): Promise { - await this.clickRowToggle({ isAnchorRow, rowIndex }); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - return await retry.try(async () => { - const row = isAnchorRow ? await this.getAnchorRow() : (await this.getBodyRows())[rowIndex]; - const detailsRow = await row.findByXpath( - './following-sibling::*[@data-test-subj="docTableDetailsRow"]' - ); - return detailsRow.findByTestSubject('~docViewer'); - }); - } - } - - return new DocTable(); + .map((field: any) => $(field).text()) + ); + } + + public async getHeaderFields(selector?: string): Promise { + const table = await this.getTable(selector); + const $ = await table.parseDomContent(); + return $.findTestSubjects('~docTableHeaderField') + .toArray() + .map((field: any) => $(field).text().trim()); + } + + public async getHeaders(selector?: string): Promise { + return this.getHeaderFields(selector); + } + + public async getTableDocViewRow( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + return await detailsRow.findByTestSubject(`~tableDocViewRow-${fieldName}`); + } + + public async getAddInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~addInclusiveFilterButton`); + } + + public async addInclusiveFilter(detailsRow: WebElementWrapper, fieldName: string): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async getRemoveInclusiveFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`); + } + + public async removeInclusiveFilter( + detailsRow: WebElementWrapper, + fieldName: string + ): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async getAddExistsFilterButton( + tableDocViewRow: WebElementWrapper + ): Promise { + return await tableDocViewRow.findByTestSubject(`~addExistsFilterButton`); + } + + public async addExistsFilter(detailsRow: WebElementWrapper, fieldName: string): Promise { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddExistsFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } + + public async toggleRowExpanded({ + isAnchorRow = false, + rowIndex = 0, + }: SelectOptions = {}): Promise { + await this.clickRowToggle({ isAnchorRow, rowIndex }); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + return await this.retry.try(async () => { + const row = isAnchorRow ? await this.getAnchorRow() : (await this.getBodyRows())[rowIndex]; + const detailsRow = await row.findByXpath( + './following-sibling::*[@data-test-subj="docTableDetailsRow"]' + ); + return detailsRow.findByTestSubject('~docViewer'); + }); + } } diff --git a/test/functional/services/embedding.ts b/test/functional/services/embedding.ts index f3d5340e41fa6a..e394aff19ab8b6 100644 --- a/test/functional/services/embedding.ts +++ b/test/functional/services/embedding.ts @@ -6,24 +6,20 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function EmbeddingProvider({ getService, getPageObjects }: FtrProviderContext) { - const browser = getService('browser'); - const log = getService('log'); - const PageObjects = getPageObjects(['header']); +export class EmbeddingService extends FtrService { + private readonly browser = this.ctx.getService('browser'); + private readonly log = this.ctx.getService('log'); + private readonly PageObjects = this.ctx.getPageObjects(['header']); - class Embedding { - /** - * Opens current page in embeded mode - */ - public async openInEmbeddedMode(): Promise { - const currentUrl = await browser.getCurrentUrl(); - log.debug(`Opening in embedded mode: ${currentUrl}`); - await browser.get(`${currentUrl}&embed=true`); - await PageObjects.header.waitUntilLoadingHasFinished(); - } + /** + * Opens current page in embeded mode + */ + public async openInEmbeddedMode(): Promise { + const currentUrl = await this.browser.getCurrentUrl(); + this.log.debug(`Opening in embedded mode: ${currentUrl}`); + await this.browser.get(`${currentUrl}&embed=true`); + await this.PageObjects.header.waitUntilLoadingHasFinished(); } - - return new Embedding(); } diff --git a/test/functional/services/field_editor.ts b/test/functional/services/field_editor.ts index 342e2afec28d3c..c74c229cd11c7c 100644 --- a/test/functional/services/field_editor.ts +++ b/test/functional/services/field_editor.ts @@ -6,51 +6,47 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function FieldEditorProvider({ getService }: FtrProviderContext) { - const browser = getService('browser'); - const testSubjects = getService('testSubjects'); +export class FieldEditorService extends FtrService { + private readonly browser = this.ctx.getService('browser'); + private readonly testSubjects = this.ctx.getService('testSubjects'); - class FieldEditor { - public async setName(name: string) { - await testSubjects.setValue('nameField > input', name); - } - public async enableCustomLabel() { - await testSubjects.setEuiSwitch('customLabelRow > toggle', 'check'); - } - public async setCustomLabel(name: string) { - await testSubjects.setValue('customLabelRow > input', name); - } - public async enableValue() { - await testSubjects.setEuiSwitch('valueRow > toggle', 'check'); - } - public async disableValue() { - await testSubjects.setEuiSwitch('valueRow > toggle', 'uncheck'); - } - public async typeScript(script: string) { - const editor = await (await testSubjects.find('valueRow')).findByClassName( - 'react-monaco-editor-container' - ); - const textarea = await editor.findByClassName('monaco-mouse-cursor-text'); - - await textarea.click(); - await browser.pressKeys(script); - } - public async save() { - await testSubjects.click('fieldSaveButton'); - } + public async setName(name: string) { + await this.testSubjects.setValue('nameField > input', name); + } + public async enableCustomLabel() { + await this.testSubjects.setEuiSwitch('customLabelRow > toggle', 'check'); + } + public async setCustomLabel(name: string) { + await this.testSubjects.setValue('customLabelRow > input', name); + } + public async enableValue() { + await this.testSubjects.setEuiSwitch('valueRow > toggle', 'check'); + } + public async disableValue() { + await this.testSubjects.setEuiSwitch('valueRow > toggle', 'uncheck'); + } + public async typeScript(script: string) { + const editor = await (await this.testSubjects.find('valueRow')).findByClassName( + 'react-monaco-editor-container' + ); + const textarea = await editor.findByClassName('monaco-mouse-cursor-text'); - public async confirmSave() { - await testSubjects.setValue('saveModalConfirmText', 'change'); - await testSubjects.click('confirmModalConfirmButton'); - } + await textarea.click(); + await this.browser.pressKeys(script); + } + public async save() { + await this.testSubjects.click('fieldSaveButton'); + } - public async confirmDelete() { - await testSubjects.setValue('deleteModalConfirmText', 'remove'); - await testSubjects.click('confirmModalConfirmButton'); - } + public async confirmSave() { + await this.testSubjects.setValue('saveModalConfirmText', 'change'); + await this.testSubjects.click('confirmModalConfirmButton'); } - return new FieldEditor(); + public async confirmDelete() { + await this.testSubjects.setValue('deleteModalConfirmText', 'remove'); + await this.testSubjects.click('confirmModalConfirmButton'); + } } diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index 1ffa5c94b5fc45..5f20d3d4f8b7b5 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -7,200 +7,196 @@ */ import classNames from 'classnames'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export function FilterBarProvider({ getService, getPageObjects }: FtrProviderContext) { - const comboBox = getService('comboBox'); - const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects(['common', 'header']); - - class FilterBar { - /** - * Checks if specified filter exists - * - * @param key field name - * @param value filter value - * @param enabled filter status - * @param pinned filter pinned status - * @param negated filter including or excluding value - */ - public async hasFilter( - key: string, - value: string, - enabled: boolean = true, - pinned: boolean = false, - negated: boolean = false - ): Promise { - const filterActivationState = enabled ? 'enabled' : 'disabled'; - const filterPinnedState = pinned ? 'pinned' : 'unpinned'; - const filterNegatedState = negated ? 'filter-negated' : ''; - return testSubjects.exists( - classNames( - 'filter', - `filter-${filterActivationState}`, - key !== '' && `filter-key-${key}`, - value !== '' && `filter-value-${value}`, - `filter-${filterPinnedState}`, - filterNegatedState - ), - { - allowHidden: true, - } - ); - } +import { FtrService } from '../ftr_provider_context'; + +export class FilterBarService extends FtrService { + private readonly comboBox = this.ctx.getService('comboBox'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly PageObjects = this.ctx.getPageObjects(['common', 'header']); + + /** + * Checks if specified filter exists + * + * @param key field name + * @param value filter value + * @param enabled filter status + * @param pinned filter pinned status + * @param negated filter including or excluding value + */ + public async hasFilter( + key: string, + value: string, + enabled: boolean = true, + pinned: boolean = false, + negated: boolean = false + ): Promise { + const filterActivationState = enabled ? 'enabled' : 'disabled'; + const filterPinnedState = pinned ? 'pinned' : 'unpinned'; + const filterNegatedState = negated ? 'filter-negated' : ''; + return this.testSubjects.exists( + classNames( + 'filter', + `filter-${filterActivationState}`, + key !== '' && `filter-key-${key}`, + value !== '' && `filter-value-${value}`, + `filter-${filterPinnedState}`, + filterNegatedState + ), + { + allowHidden: true, + } + ); + } - /** - * Removes specified filter - * - * @param key field name - */ - public async removeFilter(key: string): Promise { - await testSubjects.click(`~filter & ~filter-key-${key}`); - await testSubjects.click(`deleteFilter`); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } + /** + * Removes specified filter + * + * @param key field name + */ + public async removeFilter(key: string): Promise { + await this.testSubjects.click(`~filter & ~filter-key-${key}`); + await this.testSubjects.click(`deleteFilter`); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } - /** - * Removes all filters - */ - public async removeAllFilters(): Promise { - await testSubjects.click('showFilterActions'); - await testSubjects.click('removeAllFilters'); - await PageObjects.header.waitUntilLoadingHasFinished(); - await PageObjects.common.waitUntilUrlIncludes('filters:!()'); - } + /** + * Removes all filters + */ + public async removeAllFilters(): Promise { + await this.testSubjects.click('showFilterActions'); + await this.testSubjects.click('removeAllFilters'); + await this.PageObjects.header.waitUntilLoadingHasFinished(); + await this.PageObjects.common.waitUntilUrlIncludes('filters:!()'); + } - /** - * Changes filter active status - * - * @param key field name - */ - public async toggleFilterEnabled(key: string): Promise { - await testSubjects.click(`~filter & ~filter-key-${key}`); - await testSubjects.click(`disableFilter`); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } + /** + * Changes filter active status + * + * @param key field name + */ + public async toggleFilterEnabled(key: string): Promise { + await this.testSubjects.click(`~filter & ~filter-key-${key}`); + await this.testSubjects.click(`disableFilter`); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } - public async toggleFilterPinned(key: string): Promise { - await testSubjects.click(`~filter & ~filter-key-${key}`); - await testSubjects.click(`pinFilter`); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } + public async toggleFilterPinned(key: string): Promise { + await this.testSubjects.click(`~filter & ~filter-key-${key}`); + await this.testSubjects.click(`pinFilter`); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } - public async isFilterPinned(key: string): Promise { - const filter = await testSubjects.find(`~filter & ~filter-key-${key}`); - return (await filter.getAttribute('data-test-subj')).includes('filter-pinned'); - } + public async isFilterPinned(key: string): Promise { + const filter = await this.testSubjects.find(`~filter & ~filter-key-${key}`); + return (await filter.getAttribute('data-test-subj')).includes('filter-pinned'); + } - public async getFilterCount(): Promise { - const filters = await testSubjects.findAll('~filter'); - return filters.length; - } + public async getFilterCount(): Promise { + const filters = await this.testSubjects.findAll('~filter'); + return filters.length; + } - /** - * Adds a filter to the filter bar. - * - * @param {string} field The name of the field the filter should be applied for. - * @param {string} operator A valid operator for that fields, e.g. "is one of", "is", "exists", etc. - * @param {string[]|string} values The remaining parameters are the values passed into the individual - * value input fields, i.e. the third parameter into the first input field, the fourth into the second, etc. - * Each value itself can be an array, in case you want to enter multiple values into one field (e.g. for "is one of"): - * @example - * // Add a plain single value - * filterBar.addFilter('country', 'is', 'NL'); - * // Add an exists filter - * filterBar.addFilter('country', 'exists'); - * // Add a range filter for a numeric field - * filterBar.addFilter('bytes', 'is between', '500', '1000'); - * // Add a filter containing multiple values - * filterBar.addFilter('extension', 'is one of', ['jpg', 'png']); - */ - public async addFilter(field: string, operator: string, ...values: any): Promise { - await testSubjects.click('addFilter'); - await comboBox.set('filterFieldSuggestionList', field); - await comboBox.set('filterOperatorList', operator); - const params = await testSubjects.find('filterParams'); - const paramsComboBoxes = await params.findAllByCssSelector( - '[data-test-subj~="filterParamsComboBox"]', - 1000 - ); - const paramFields = await params.findAllByTagName('input', 1000); - for (let i = 0; i < values.length; i++) { - let fieldValues = values[i]; - if (!Array.isArray(fieldValues)) { - fieldValues = [fieldValues]; - } + /** + * Adds a filter to the filter bar. + * + * @param {string} field The name of the field the filter should be applied for. + * @param {string} operator A valid operator for that fields, e.g. "is one of", "is", "exists", etc. + * @param {string[]|string} values The remaining parameters are the values passed into the individual + * value input fields, i.e. the third parameter into the first input field, the fourth into the second, etc. + * Each value itself can be an array, in case you want to enter multiple values into one field (e.g. for "is one of"): + * @example + * // Add a plain single value + * filterBar.addFilter('country', 'is', 'NL'); + * // Add an exists filter + * filterBar.addFilter('country', 'exists'); + * // Add a range filter for a numeric field + * filterBar.addFilter('bytes', 'is between', '500', '1000'); + * // Add a filter containing multiple values + * filterBar.addFilter('extension', 'is one of', ['jpg', 'png']); + */ + public async addFilter(field: string, operator: string, ...values: any): Promise { + await this.testSubjects.click('addFilter'); + await this.comboBox.set('filterFieldSuggestionList', field); + await this.comboBox.set('filterOperatorList', operator); + const params = await this.testSubjects.find('filterParams'); + const paramsComboBoxes = await params.findAllByCssSelector( + '[data-test-subj~="filterParamsComboBox"]', + 1000 + ); + const paramFields = await params.findAllByTagName('input', 1000); + for (let i = 0; i < values.length; i++) { + let fieldValues = values[i]; + if (!Array.isArray(fieldValues)) { + fieldValues = [fieldValues]; + } - if (paramsComboBoxes && paramsComboBoxes.length > 0) { - for (let j = 0; j < fieldValues.length; j++) { - await comboBox.setElement(paramsComboBoxes[i], fieldValues[j]); - } - } else if (paramFields && paramFields.length > 0) { - for (let j = 0; j < fieldValues.length; j++) { - await paramFields[i].type(fieldValues[j]); - } + if (paramsComboBoxes && paramsComboBoxes.length > 0) { + for (let j = 0; j < fieldValues.length; j++) { + await this.comboBox.setElement(paramsComboBoxes[i], fieldValues[j]); + } + } else if (paramFields && paramFields.length > 0) { + for (let j = 0; j < fieldValues.length; j++) { + await paramFields[i].type(fieldValues[j]); } } - await testSubjects.click('saveFilter'); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); - } - - /** - * Activates filter editing - * @param key field name - * @param value field value - */ - public async clickEditFilter(key: string, value: string): Promise { - await testSubjects.click(`~filter & ~filter-key-${key} & ~filter-value-${value}`); - await testSubjects.click(`editFilter`); - await PageObjects.header.awaitGlobalLoadingIndicatorHidden(); } + await this.testSubjects.click('saveFilter'); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } - /** - * Returns available phrases in the filter - */ - public async getFilterEditorSelectedPhrases(): Promise { - return await comboBox.getComboBoxSelectedOptions('~filterParamsComboBox'); - } + /** + * Activates filter editing + * @param key field name + * @param value field value + */ + public async clickEditFilter(key: string, value: string): Promise { + await this.testSubjects.click(`~filter & ~filter-key-${key} & ~filter-value-${value}`); + await this.testSubjects.click(`editFilter`); + await this.PageObjects.header.awaitGlobalLoadingIndicatorHidden(); + } - /** - * Returns available fields in the filter - */ - public async getFilterEditorFields(): Promise { - const optionsString = await comboBox.getOptionsList('filterFieldSuggestionList'); - return optionsString.split('\n'); - } + /** + * Returns available phrases in the filter + */ + public async getFilterEditorSelectedPhrases(): Promise { + return await this.comboBox.getComboBoxSelectedOptions('~filterParamsComboBox'); + } - /** - * Closes field editor modal window - */ - public async ensureFieldEditorModalIsClosed(): Promise { - const cancelSaveFilterModalButtonExists = await testSubjects.exists('cancelSaveFilter'); - if (cancelSaveFilterModalButtonExists) { - await testSubjects.click('cancelSaveFilter'); - } - await testSubjects.waitForDeleted('cancelSaveFilter'); - } + /** + * Returns available fields in the filter + */ + public async getFilterEditorFields(): Promise { + const optionsString = await this.comboBox.getOptionsList('filterFieldSuggestionList'); + return optionsString.split('\n'); + } - /** - * Returns comma-separated list of index patterns - */ - public async getIndexPatterns(): Promise { - await testSubjects.click('addFilter'); - const indexPatterns = await comboBox.getOptionsList('filterIndexPatternsSelect'); - await this.ensureFieldEditorModalIsClosed(); - return indexPatterns.trim().split('\n').join(','); + /** + * Closes field editor modal window + */ + public async ensureFieldEditorModalIsClosed(): Promise { + const cancelSaveFilterModalButtonExists = await this.testSubjects.exists('cancelSaveFilter'); + if (cancelSaveFilterModalButtonExists) { + await this.testSubjects.click('cancelSaveFilter'); } + await this.testSubjects.waitForDeleted('cancelSaveFilter'); + } - /** - * Adds new index pattern filter - * @param indexPatternTitle - */ - public async selectIndexPattern(indexPatternTitle: string): Promise { - await testSubjects.click('addFilter'); - await comboBox.set('filterIndexPatternsSelect', indexPatternTitle); - } + /** + * Returns comma-separated list of index patterns + */ + public async getIndexPatterns(): Promise { + await this.testSubjects.click('addFilter'); + const indexPatterns = await this.comboBox.getOptionsList('filterIndexPatternsSelect'); + await this.ensureFieldEditorModalIsClosed(); + return indexPatterns.trim().split('\n').join(','); } - return new FilterBar(); + /** + * Adds new index pattern filter + * @param indexPatternTitle + */ + public async selectIndexPattern(indexPatternTitle: string): Promise { + await this.testSubjects.click('addFilter'); + await this.comboBox.set('filterIndexPatternsSelect', indexPatternTitle); + } } diff --git a/test/functional/services/flyout.ts b/test/functional/services/flyout.ts index 2e10bf757ac7a6..e7b57dc0fb66bf 100644 --- a/test/functional/services/flyout.ts +++ b/test/functional/services/flyout.ts @@ -6,50 +6,46 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; - -export function FlyoutProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const log = getService('log'); - const retry = getService('retry'); - - class Flyout { - public async close(dataTestSubj: string): Promise { - log.debug('Closing flyout', dataTestSubj); - const flyoutElement = await testSubjects.find(dataTestSubj); - const closeBtn = await flyoutElement.findByCssSelector('[aria-label*="Close"]'); - await closeBtn.click(); - await retry.waitFor( - 'flyout closed', - async () => !(await testSubjects.exists(dataTestSubj, { timeout: 1000 })) - ); - } +import { FtrService } from '../ftr_provider_context'; + +export class FlyoutService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly find = this.ctx.getService('find'); + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + + public async close(dataTestSubj: string): Promise { + this.log.debug('Closing flyout', dataTestSubj); + const flyoutElement = await this.testSubjects.find(dataTestSubj); + const closeBtn = await flyoutElement.findByCssSelector('[aria-label*="Close"]'); + await closeBtn.click(); + await this.retry.waitFor( + 'flyout closed', + async () => !(await this.testSubjects.exists(dataTestSubj, { timeout: 1000 })) + ); + } - public async ensureClosed(dataTestSubj: string): Promise { - if (await testSubjects.exists(dataTestSubj, { timeout: 1000 })) { - await this.close(dataTestSubj); - } + public async ensureClosed(dataTestSubj: string): Promise { + if (await this.testSubjects.exists(dataTestSubj, { timeout: 1000 })) { + await this.close(dataTestSubj); } + } - public async ensureAllClosed(): Promise { - const flyoutElements = await find.allByCssSelector('.euiFlyout'); - - if (!flyoutElements.length) { - return; - } + public async ensureAllClosed(): Promise { + const flyoutElements = await this.find.allByCssSelector('.euiFlyout'); - for (let i = 0; i < flyoutElements.length; i++) { - const closeBtn = await flyoutElements[i].findByCssSelector('[aria-label*="Close"]'); - await closeBtn.click(); - } + if (!flyoutElements.length) { + return; + } - await retry.waitFor( - 'all flyouts to be closed', - async () => (await find.allByCssSelector('.euiFlyout')).length === 0 - ); + for (let i = 0; i < flyoutElements.length; i++) { + const closeBtn = await flyoutElements[i].findByCssSelector('[aria-label*="Close"]'); + await closeBtn.click(); } - } - return new Flyout(); + await this.retry.waitFor( + 'all flyouts to be closed', + async () => (await this.find.allByCssSelector('.euiFlyout')).length === 0 + ); + } } diff --git a/test/functional/services/global_nav.ts b/test/functional/services/global_nav.ts index ec404bcb068e94..4ce8ed6cb79dde 100644 --- a/test/functional/services/global_nav.ts +++ b/test/functional/services/global_nav.ts @@ -7,50 +7,49 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function GlobalNavProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); +export class GlobalNavService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); - class GlobalNav { - public async moveMouseToLogo(): Promise { - await testSubjects.moveMouseTo('headerGlobalNav > logo'); - } - - public async clickLogo(): Promise { - return await testSubjects.click('headerGlobalNav > logo'); - } + public async moveMouseToLogo(): Promise { + await this.testSubjects.moveMouseTo('headerGlobalNav > logo'); + } - public async clickNewsfeed(): Promise { - return await testSubjects.click('headerGlobalNav > newsfeed'); - } + public async clickLogo(): Promise { + return await this.testSubjects.click('headerGlobalNav > logo'); + } - public async exists(): Promise { - return await testSubjects.exists('headerGlobalNav'); - } + public async clickNewsfeed(): Promise { + return await this.testSubjects.click('headerGlobalNav > newsfeed'); + } - public async getFirstBreadcrumb(): Promise { - return await testSubjects.getVisibleText( - 'headerGlobalNav > breadcrumbs > ~breadcrumb & ~first' - ); - } + public async exists(): Promise { + return await this.testSubjects.exists('headerGlobalNav'); + } - public async getLastBreadcrumb(): Promise { - return await testSubjects.getVisibleText( - 'headerGlobalNav > breadcrumbs > ~breadcrumb & ~last' - ); - } + public async getFirstBreadcrumb(): Promise { + return await this.testSubjects.getVisibleText( + 'headerGlobalNav > breadcrumbs > ~breadcrumb & ~first' + ); + } - public async badgeExistsOrFail(expectedLabel: string): Promise { - await testSubjects.existOrFail('headerBadge'); - const actualLabel = await testSubjects.getAttribute('headerBadge', 'data-test-badge-label'); - expect(actualLabel.toUpperCase()).to.equal(expectedLabel.toUpperCase()); - } + public async getLastBreadcrumb(): Promise { + return await this.testSubjects.getVisibleText( + 'headerGlobalNav > breadcrumbs > ~breadcrumb & ~last' + ); + } - public async badgeMissingOrFail(): Promise { - await testSubjects.missingOrFail('headerBadge'); - } + public async badgeExistsOrFail(expectedLabel: string): Promise { + await this.testSubjects.existOrFail('headerBadge'); + const actualLabel = await this.testSubjects.getAttribute( + 'headerBadge', + 'data-test-badge-label' + ); + expect(actualLabel.toUpperCase()).to.equal(expectedLabel.toUpperCase()); } - return new GlobalNav(); + public async badgeMissingOrFail(): Promise { + await this.testSubjects.missingOrFail('headerBadge'); + } } diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 0dd7f20debcbdf..a509141390f676 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -8,82 +8,82 @@ import { services as commonServiceProviders } from '../../common/services'; -import { AppsMenuProvider } from './apps_menu'; +import { AppsMenuService } from './apps_menu'; import { BrowserProvider, FailureDebuggingProvider, FindProvider, - ScreenshotsProvider, - SnapshotsProvider, - TestSubjectsProvider, + ScreenshotsService, + SnapshotsService, + TestSubjects, } from './common'; -import { ComboBoxProvider } from './combo_box'; +import { ComboBoxService } from './combo_box'; import { - DashboardAddPanelProvider, - DashboardReplacePanelProvider, - DashboardExpectProvider, - DashboardPanelActionsProvider, - DashboardVisualizationProvider, + DashboardAddPanelService, + DashboardReplacePanelService, + DashboardExpectService, + DashboardPanelActionsService, + DashboardVisualizationsService, } from './dashboard'; -import { DocTableProvider } from './doc_table'; -import { EmbeddingProvider } from './embedding'; -import { FilterBarProvider } from './filter_bar'; -import { FlyoutProvider } from './flyout'; -import { GlobalNavProvider } from './global_nav'; -import { InspectorProvider } from './inspector'; -import { FieldEditorProvider } from './field_editor'; -import { ManagementMenuProvider } from './management'; -import { QueryBarProvider } from './query_bar'; +import { DocTableService } from './doc_table'; +import { EmbeddingService } from './embedding'; +import { FilterBarService } from './filter_bar'; +import { FlyoutService } from './flyout'; +import { GlobalNavService } from './global_nav'; +import { InspectorService } from './inspector'; +import { FieldEditorService } from './field_editor'; +import { ManagementMenuService } from './management'; +import { QueryBarService } from './query_bar'; import { RemoteProvider } from './remote'; -import { RenderableProvider } from './renderable'; -import { ToastsProvider } from './toasts'; -import { DataGridProvider } from './data_grid'; +import { RenderableService } from './renderable'; +import { ToastsService } from './toasts'; +import { DataGridService } from './data_grid'; import { - PieChartProvider, - ElasticChartProvider, - VegaDebugInspectorViewProvider, + PieChartService, + ElasticChartService, + VegaDebugInspectorViewService, } from './visualizations'; -import { ListingTableProvider } from './listing_table'; -import { SavedQueryManagementComponentProvider } from './saved_query_management_component'; +import { ListingTableService } from './listing_table'; +import { SavedQueryManagementComponentService } from './saved_query_management_component'; import { KibanaSupertestProvider } from './supertest'; -import { MenuToggleProvider } from './menu_toggle'; +import { MenuToggleService } from './menu_toggle'; import { MonacoEditorProvider } from './monaco_editor'; export const services = { ...commonServiceProviders, __webdriver__: RemoteProvider, - filterBar: FilterBarProvider, - queryBar: QueryBarProvider, + filterBar: FilterBarService, + queryBar: QueryBarService, find: FindProvider, - testSubjects: TestSubjectsProvider, - docTable: DocTableProvider, - screenshots: ScreenshotsProvider, - snapshots: SnapshotsProvider, - dashboardVisualizations: DashboardVisualizationProvider, - dashboardExpect: DashboardExpectProvider, + testSubjects: TestSubjects, + docTable: DocTableService, + screenshots: ScreenshotsService, + snapshots: SnapshotsService, failureDebugging: FailureDebuggingProvider, - listingTable: ListingTableProvider, - dashboardAddPanel: DashboardAddPanelProvider, - dashboardReplacePanel: DashboardReplacePanelProvider, - dashboardPanelActions: DashboardPanelActionsProvider, - flyout: FlyoutProvider, - comboBox: ComboBoxProvider, - dataGrid: DataGridProvider, - embedding: EmbeddingProvider, - renderable: RenderableProvider, + listingTable: ListingTableService, + dashboardVisualizations: DashboardVisualizationsService, + dashboardExpect: DashboardExpectService, + dashboardAddPanel: DashboardAddPanelService, + dashboardReplacePanel: DashboardReplacePanelService, + dashboardPanelActions: DashboardPanelActionsService, + flyout: FlyoutService, + comboBox: ComboBoxService, + dataGrid: DataGridService, + embedding: EmbeddingService, + renderable: RenderableService, browser: BrowserProvider, - pieChart: PieChartProvider, - inspector: InspectorProvider, - fieldEditor: FieldEditorProvider, - vegaDebugInspector: VegaDebugInspectorViewProvider, - appsMenu: AppsMenuProvider, - globalNav: GlobalNavProvider, - toasts: ToastsProvider, - savedQueryManagementComponent: SavedQueryManagementComponentProvider, - elasticChart: ElasticChartProvider, + pieChart: PieChartService, + inspector: InspectorService, + fieldEditor: FieldEditorService, + vegaDebugInspector: VegaDebugInspectorViewService, + appsMenu: AppsMenuService, + globalNav: GlobalNavService, + toasts: ToastsService, + savedQueryManagementComponent: SavedQueryManagementComponentService, + elasticChart: ElasticChartService, supertest: KibanaSupertestProvider, - managementMenu: ManagementMenuProvider, + managementMenu: ManagementMenuService, monacoEditor: MonacoEditorProvider, - MenuToggle: MenuToggleProvider, + menuToggle: MenuToggleService, }; diff --git a/test/functional/services/inspector.ts b/test/functional/services/inspector.ts index c9cf159d0d38e1..dc46db458501bf 100644 --- a/test/functional/services/inspector.ts +++ b/test/functional/services/inspector.ts @@ -7,234 +7,230 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export function InspectorProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const renderable = getService('renderable'); - const flyout = getService('flyout'); - const testSubjects = getService('testSubjects'); - const find = getService('find'); - - class Inspector { - private async getIsEnabled(): Promise { - const ariaDisabled = await testSubjects.getAttribute('openInspectorButton', 'disabled'); - return ariaDisabled !== 'true'; - } - - /** - * Asserts that inspector is enabled - */ - public async expectIsEnabled(): Promise { - await retry.try(async () => { - const isEnabled = await this.getIsEnabled(); - expect(isEnabled).to.be(true); - }); - } +import { FtrService } from '../ftr_provider_context'; + +export class InspectorService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly renderable = this.ctx.getService('renderable'); + private readonly flyout = this.ctx.getService('flyout'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly find = this.ctx.getService('find'); + + private async getIsEnabled(): Promise { + const ariaDisabled = await this.testSubjects.getAttribute('openInspectorButton', 'disabled'); + return ariaDisabled !== 'true'; + } - /** - * Asserts that inspector is disabled - */ - public async expectIsNotEnabled(): Promise { - await retry.try(async () => { - const isEnabled = await this.getIsEnabled(); - expect(isEnabled).to.be(false); - }); - } + /** + * Asserts that inspector is enabled + */ + public async expectIsEnabled(): Promise { + await this.retry.try(async () => { + const isEnabled = await this.getIsEnabled(); + expect(isEnabled).to.be(true); + }); + } - /** - * Opens inspector panel - */ - public async open(): Promise { - log.debug('Inspector.open'); - const isOpen = await testSubjects.exists('inspectorPanel'); - if (!isOpen) { - await retry.try(async () => { - await testSubjects.click('openInspectorButton'); - await testSubjects.exists('inspectorPanel'); - }); - } - } + /** + * Asserts that inspector is disabled + */ + public async expectIsNotEnabled(): Promise { + await this.retry.try(async () => { + const isEnabled = await this.getIsEnabled(); + expect(isEnabled).to.be(false); + }); + } - /** - * Closes inspector panel - */ - public async close(): Promise { - log.debug('Close Inspector'); - let isOpen = await testSubjects.exists('inspectorPanel'); - if (isOpen) { - await retry.try(async () => { - await flyout.close('inspectorPanel'); - isOpen = await testSubjects.exists('inspectorPanel'); - if (isOpen) { - throw new Error('Failed to close inspector'); - } - }); - } + /** + * Opens inspector panel + */ + public async open(): Promise { + this.log.debug('Inspector.open'); + const isOpen = await this.testSubjects.exists('inspectorPanel'); + if (!isOpen) { + await this.retry.try(async () => { + await this.testSubjects.click('openInspectorButton'); + await this.testSubjects.exists('inspectorPanel'); + }); } + } - /** - * Asserts data on inspector panel - * @param expectedData - */ - public async expectTableData(expectedData: string[][]): Promise { - log.debug(`Inspector.expectTableData(${expectedData.join(',')})`); - const data = await this.getTableData(); - expect(data).to.eql(expectedData); + /** + * Closes inspector panel + */ + public async close(): Promise { + this.log.debug('Close Inspector'); + let isOpen = await this.testSubjects.exists('inspectorPanel'); + if (isOpen) { + await this.retry.try(async () => { + await this.flyout.close('inspectorPanel'); + isOpen = await this.testSubjects.exists('inspectorPanel'); + if (isOpen) { + throw new Error('Failed to close inspector'); + } + }); } + } - /** - * Sets table page size - * @param size rows count - */ - public async setTablePageSize(size: number): Promise { - const panel = await testSubjects.find('inspectorPanel'); - await find.clickByButtonText('Rows per page: 20', panel); - // The buttons for setting table page size are in a popover element. This popover - // element appears as if it's part of the inspectorPanel but it's really attached - // to the body element by a portal. - const tableSizesPopover = await find.byCssSelector('.euiPanel .euiContextMenuPanel'); - await find.clickByButtonText(`${size} rows`, tableSizesPopover); - } + /** + * Asserts data on inspector panel + * @param expectedData + */ + public async expectTableData(expectedData: string[][]): Promise { + this.log.debug(`Inspector.expectTableData(${expectedData.join(',')})`); + const data = await this.getTableData(); + expect(data).to.eql(expectedData); + } - /** - * Returns table data in nested array format - */ - public async getTableData(): Promise { - // TODO: we should use datat-test-subj=inspectorTable as soon as EUI supports it - const inspectorPanel = await testSubjects.find('inspectorPanel'); - const tableBody = await retry.try(async () => inspectorPanel.findByTagName('tbody')); - const $ = await tableBody.parseDomContent(); - return $('tr') - .toArray() - .map((tr) => { - return $(tr) - .find('td') - .toArray() - .map((cell) => { - // if this is an EUI table, filter down to the specific cell content - // otherwise this will include mobile-specific header information - const euiTableCellContent = $(cell).find('.euiTableCellContent'); - - if (euiTableCellContent.length > 0) { - return $(cell).find('.euiTableCellContent').text().trim(); - } else { - return $(cell).text().trim(); - } - }); - }); - } + /** + * Sets table page size + * @param size rows count + */ + public async setTablePageSize(size: number): Promise { + const panel = await this.testSubjects.find('inspectorPanel'); + await this.find.clickByButtonText('Rows per page: 20', panel); + // The buttons for setting table page size are in a popover element. This popover + // element appears as if it's part of the inspectorPanel but it's really attached + // to the body element by a portal. + const tableSizesPopover = await this.find.byCssSelector('.euiPanel .euiContextMenuPanel'); + await this.find.clickByButtonText(`${size} rows`, tableSizesPopover); + } - /** - * Returns table headers - */ - public async getTableHeaders(): Promise { - log.debug('Inspector.getTableHeaders'); - // TODO: we should use datat-test-subj=inspectorTable as soon as EUI supports it - const dataTableHeader = await retry.try(async () => { - const inspectorPanel = await testSubjects.find('inspectorPanel'); - return await inspectorPanel.findByTagName('thead'); + /** + * Returns table data in nested array format + */ + public async getTableData(): Promise { + // TODO: we should use datat-test-subj=inspectorTable as soon as EUI supports it + const inspectorPanel = await this.testSubjects.find('inspectorPanel'); + const tableBody = await this.retry.try(async () => inspectorPanel.findByTagName('tbody')); + const $ = await tableBody.parseDomContent(); + return $('tr') + .toArray() + .map((tr) => { + return $(tr) + .find('td') + .toArray() + .map((cell) => { + // if this is an EUI table, filter down to the specific cell content + // otherwise this will include mobile-specific header information + const euiTableCellContent = $(cell).find('.euiTableCellContent'); + + if (euiTableCellContent.length > 0) { + return $(cell).find('.euiTableCellContent').text().trim(); + } else { + return $(cell).text().trim(); + } + }); }); - const $ = await dataTableHeader.parseDomContent(); - return $('th span.euiTableCellContent__text') - .toArray() - .map((cell) => $(cell).text().trim()); - } + } - /** - * Asserts table headers - * @param expected expected headers - */ - public async expectTableHeaders(expected: string[]): Promise { - await retry.try(async () => { - const headers = await this.getTableHeaders(); - expect(headers).to.eql(expected); - }); - } + /** + * Returns table headers + */ + public async getTableHeaders(): Promise { + this.log.debug('Inspector.getTableHeaders'); + // TODO: we should use datat-test-subj=inspectorTable as soon as EUI supports it + const dataTableHeader = await this.retry.try(async () => { + const inspectorPanel = await this.testSubjects.find('inspectorPanel'); + return await inspectorPanel.findByTagName('thead'); + }); + const $ = await dataTableHeader.parseDomContent(); + return $('th span.euiTableCellContent__text') + .toArray() + .map((cell) => $(cell).text().trim()); + } - /** - * Filters table for value by clicking specified cell - * @param column column index - * @param row row index - */ - public async filterForTableCell(column: string | number, row: string | number): Promise { - await retry.try(async () => { - const table = await testSubjects.find('inspectorTable'); - const cell = await table.findByCssSelector( - `tbody tr:nth-child(${row}) td:nth-child(${column})` - ); - await cell.moveMouseTo(); - const filterBtn = await testSubjects.findDescendant('filterForInspectorCellValue', cell); - await filterBtn.click(); - }); - await renderable.waitForRender(); - } + /** + * Asserts table headers + * @param expected expected headers + */ + public async expectTableHeaders(expected: string[]): Promise { + await this.retry.try(async () => { + const headers = await this.getTableHeaders(); + expect(headers).to.eql(expected); + }); + } - /** - * Filters out table by clicking specified cell - * @param column column index - * @param row row index - */ - public async filterOutTableCell(column: string | number, row: string | number): Promise { - await retry.try(async () => { - const table = await testSubjects.find('inspectorTable'); - const cell = await table.findByCssSelector( - `tbody tr:nth-child(${row}) td:nth-child(${column})` - ); - await cell.moveMouseTo(); - const filterBtn = await testSubjects.findDescendant('filterOutInspectorCellValue', cell); - await filterBtn.click(); - }); - await renderable.waitForRender(); - } + /** + * Filters table for value by clicking specified cell + * @param column column index + * @param row row index + */ + public async filterForTableCell(column: string | number, row: string | number): Promise { + await this.retry.try(async () => { + const table = await this.testSubjects.find('inspectorTable'); + const cell = await table.findByCssSelector( + `tbody tr:nth-child(${row}) td:nth-child(${column})` + ); + await cell.moveMouseTo(); + const filterBtn = await this.testSubjects.findDescendant('filterForInspectorCellValue', cell); + await filterBtn.click(); + }); + await this.renderable.waitForRender(); + } - /** - * Opens inspector view - * @param viewId - */ - public async openInspectorView(viewId: string): Promise { - log.debug(`Open Inspector view ${viewId}`); - await testSubjects.click('inspectorViewChooser'); - await testSubjects.click(viewId); - } + /** + * Filters out table by clicking specified cell + * @param column column index + * @param row row index + */ + public async filterOutTableCell(column: string | number, row: string | number): Promise { + await this.retry.try(async () => { + const table = await this.testSubjects.find('inspectorTable'); + const cell = await table.findByCssSelector( + `tbody tr:nth-child(${row}) td:nth-child(${column})` + ); + await cell.moveMouseTo(); + const filterBtn = await this.testSubjects.findDescendant('filterOutInspectorCellValue', cell); + await filterBtn.click(); + }); + await this.renderable.waitForRender(); + } - /** - * Opens inspector requests view - */ - public async openInspectorRequestsView(): Promise { - await this.openInspectorView('inspectorViewChooserRequests'); - } + /** + * Opens inspector view + * @param viewId + */ + public async openInspectorView(viewId: string): Promise { + this.log.debug(`Open Inspector view ${viewId}`); + await this.testSubjects.click('inspectorViewChooser'); + await this.testSubjects.click(viewId); + } - /** - * Returns request name as the comma-separated string - */ - public async getRequestNames(): Promise { - await this.openInspectorRequestsView(); - const requestChooserExists = await testSubjects.exists('inspectorRequestChooser'); - if (requestChooserExists) { - await testSubjects.click('inspectorRequestChooser'); - const menu = await testSubjects.find('inspectorRequestChooserMenuPanel'); - const requestNames = await menu.getVisibleText(); - return requestNames.trim().split('\n').join(','); - } - - const singleRequest = await testSubjects.find('inspectorRequestName'); - return await singleRequest.getVisibleText(); - } + /** + * Opens inspector requests view + */ + public async openInspectorRequestsView(): Promise { + await this.openInspectorView('inspectorViewChooserRequests'); + } - public getOpenRequestStatisticButton() { - return testSubjects.find('inspectorRequestDetailStatistics'); - } + /** + * Returns request name as the comma-separated string + */ + public async getRequestNames(): Promise { + await this.openInspectorRequestsView(); + const requestChooserExists = await this.testSubjects.exists('inspectorRequestChooser'); + if (requestChooserExists) { + await this.testSubjects.click('inspectorRequestChooser'); + const menu = await this.testSubjects.find('inspectorRequestChooserMenuPanel'); + const requestNames = await menu.getVisibleText(); + return requestNames.trim().split('\n').join(','); + } + + const singleRequest = await this.testSubjects.find('inspectorRequestName'); + return await singleRequest.getVisibleText(); + } - public getOpenRequestDetailRequestButton() { - return testSubjects.find('inspectorRequestDetailRequest'); - } + public getOpenRequestStatisticButton() { + return this.testSubjects.find('inspectorRequestDetailStatistics'); + } - public getOpenRequestDetailResponseButton() { - return testSubjects.find('inspectorRequestDetailResponse'); - } + public getOpenRequestDetailRequestButton() { + return this.testSubjects.find('inspectorRequestDetailRequest'); } - return new Inspector(); + public getOpenRequestDetailResponseButton() { + return this.testSubjects.find('inspectorRequestDetailResponse'); + } } diff --git a/test/functional/services/listing_table.ts b/test/functional/services/listing_table.ts index 0e1fa4a7b2117f..79678cf7a812b8 100644 --- a/test/functional/services/listing_table.ts +++ b/test/functional/services/listing_table.ts @@ -7,202 +7,199 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -type AppName = 'visualize' | 'dashboard' | 'map'; +type AppName = keyof typeof PREFIX_MAP; +const PREFIX_MAP = { visualize: 'vis', dashboard: 'dashboard', map: 'map' }; -export function ListingTableProvider({ getService, getPageObjects }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const log = getService('log'); - const retry = getService('retry'); - const { common, header } = getPageObjects(['common', 'header']); - const prefixMap = { visualize: 'vis', dashboard: 'dashboard', map: 'map' }; +export class ListingTableService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly find = this.ctx.getService('find'); + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly common = this.ctx.getPageObjects(['common']).common; + private readonly header = this.ctx.getPageObjects(['header']).header; - class ListingTable { - private async getSearchFilter() { - return await testSubjects.find('tableListSearchBox'); - } + private async getSearchFilter() { + return await this.testSubjects.find('tableListSearchBox'); + } - /** - * Returns search input value on landing page - */ - public async getSearchFilterValue() { - const searchFilter = await this.getSearchFilter(); - return await searchFilter.getAttribute('value'); - } + /** + * Returns search input value on landing page + */ + public async getSearchFilterValue() { + const searchFilter = await this.getSearchFilter(); + return await searchFilter.getAttribute('value'); + } - /** - * Clears search input on landing page - */ - public async clearSearchFilter() { - const searchFilter = await this.getSearchFilter(); - await searchFilter.clearValue(); - await searchFilter.click(); - } + /** + * Clears search input on landing page + */ + public async clearSearchFilter() { + const searchFilter = await this.getSearchFilter(); + await searchFilter.clearValue(); + await searchFilter.click(); + } - private async getAllItemsNamesOnCurrentPage(): Promise { - const visualizationNames = []; - const links = await find.allByCssSelector('.euiTableRow .euiLink'); - for (let i = 0; i < links.length; i++) { - visualizationNames.push(await links[i].getVisibleText()); - } - log.debug(`Found ${visualizationNames.length} visualizations on current page`); - return visualizationNames; + private async getAllItemsNamesOnCurrentPage(): Promise { + const visualizationNames = []; + const links = await this.find.allByCssSelector('.euiTableRow .euiLink'); + for (let i = 0; i < links.length; i++) { + visualizationNames.push(await links[i].getVisibleText()); } + this.log.debug(`Found ${visualizationNames.length} visualizations on current page`); + return visualizationNames; + } - public async waitUntilTableIsLoaded() { - return retry.try(async () => { - const isLoaded = await find.existsByDisplayedByCssSelector( - '[data-test-subj="itemsInMemTable"]:not(.euiBasicTable-loading)' - ); - - if (isLoaded) { - return true; - } else { - throw new Error('Waiting'); - } - }); - } + public async waitUntilTableIsLoaded() { + return this.retry.try(async () => { + const isLoaded = await this.find.existsByDisplayedByCssSelector( + '[data-test-subj="itemsInMemTable"]:not(.euiBasicTable-loading)' + ); - /** - * Navigates through all pages on Landing page and returns array of items names - */ - public async getAllItemsNames(): Promise { - log.debug('ListingTable.getAllItemsNames'); - let morePages = true; - let visualizationNames: string[] = []; - while (morePages) { - visualizationNames = visualizationNames.concat(await this.getAllItemsNamesOnCurrentPage()); - morePages = !( - (await testSubjects.getAttribute('pagination-button-next', 'disabled')) === 'true' - ); - if (morePages) { - await testSubjects.click('pagerNextButton'); - await header.waitUntilLoadingHasFinished(); - } + if (isLoaded) { + return true; + } else { + throw new Error('Waiting'); } - return visualizationNames; - } + }); + } - /** - * Returns items count on landing page - */ - public async expectItemsCount(appName: AppName, count: number) { - await retry.try(async () => { - const elements = await find.allByCssSelector( - `[data-test-subj^="${prefixMap[appName]}ListingTitleLink"]` - ); - expect(elements.length).to.equal(count); - }); + /** + * Navigates through all pages on Landing page and returns array of items names + */ + public async getAllItemsNames(): Promise { + this.log.debug('ListingTable.getAllItemsNames'); + let morePages = true; + let visualizationNames: string[] = []; + while (morePages) { + visualizationNames = visualizationNames.concat(await this.getAllItemsNamesOnCurrentPage()); + morePages = !( + (await this.testSubjects.getAttribute('pagination-button-next', 'disabled')) === 'true' + ); + if (morePages) { + await this.testSubjects.click('pagerNextButton'); + await this.header.waitUntilLoadingHasFinished(); + } } + return visualizationNames; + } - /** - * Types name into search field on Landing page and waits till search completed - * @param name item name - */ - public async searchForItemWithName(name: string, { escape = true }: { escape?: boolean } = {}) { - log.debug(`searchForItemWithName: ${name}`); - - await retry.try(async () => { - const searchFilter = await this.getSearchFilter(); - await searchFilter.clearValue(); - await searchFilter.click(); - - if (escape) { - name = name - // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed. - .replace('-', ' ') - // Remove `[*]` from search as it is not supported by EUI Query's syntax. - .replace(/ *\[[^)]*\] */g, ''); - } - - await searchFilter.type(name); - await common.pressEnterKey(); - }); + /** + * Returns items count on landing page + */ + public async expectItemsCount(appName: AppName, count: number) { + await this.retry.try(async () => { + const elements = await this.find.allByCssSelector( + `[data-test-subj^="${PREFIX_MAP[appName]}ListingTitleLink"]` + ); + expect(elements.length).to.equal(count); + }); + } - await header.waitUntilLoadingHasFinished(); - } + /** + * Types name into search field on Landing page and waits till search completed + * @param name item name + */ + public async searchForItemWithName(name: string, { escape = true }: { escape?: boolean } = {}) { + this.log.debug(`searchForItemWithName: ${name}`); - /** - * Searches for item on Landing page and retruns items count that match `ListingTitleLink-${name}` pattern - */ - public async searchAndExpectItemsCount(appName: AppName, name: string, count: number) { - await this.searchForItemWithName(name); - await retry.try(async () => { - const links = await testSubjects.findAll( - `${prefixMap[appName]}ListingTitleLink-${name.replace(/ /g, '-')}` - ); - expect(links.length).to.equal(count); - }); - } + await this.retry.try(async () => { + const searchFilter = await this.getSearchFilter(); + await searchFilter.clearValue(); + await searchFilter.click(); - public async clickDeleteSelected() { - await testSubjects.click('deleteSelectedItems'); - } + if (escape) { + name = name + // Note: this replacement of - to space is to preserve original logic but I'm not sure why or if it's needed. + .replace('-', ' ') + // Remove `[*]` from search as it is not supported by EUI Query's syntax. + .replace(/ *\[[^)]*\] */g, ''); + } - public async clickItemCheckbox(id: string) { - await testSubjects.click(`checkboxSelectRow-${id}`); - } + await searchFilter.type(name); + await this.common.pressEnterKey(); + }); - /** - * Searches for item by name, selects checbox and deletes it - * @param name item name - * @param id row id - */ - public async deleteItem(name: string, id: string) { - await this.searchForItemWithName(name); - await this.clickItemCheckbox(id); - await this.clickDeleteSelected(); - await common.clickConfirmOnModal(); - } + await this.header.waitUntilLoadingHasFinished(); + } - /** - * Clicks item on Landing page by link name if it is present - */ - public async clickItemLink(appName: AppName, name: string) { - await testSubjects.click( - `${prefixMap[appName]}ListingTitleLink-${name.split(' ').join('-')}` + /** + * Searches for item on Landing page and retruns items count that match `ListingTitleLink-${name}` pattern + */ + public async searchAndExpectItemsCount(appName: AppName, name: string, count: number) { + await this.searchForItemWithName(name); + await this.retry.try(async () => { + const links = await this.testSubjects.findAll( + `${PREFIX_MAP[appName]}ListingTitleLink-${name.replace(/ /g, '-')}` ); - } + expect(links.length).to.equal(count); + }); + } - /** - * Checks 'SelectAll' checkbox on - */ - public async checkListingSelectAllCheckbox() { - const element = await testSubjects.find('checkboxSelectAll'); - const isSelected = await element.isSelected(); - if (!isSelected) { - log.debug(`checking checkbox "checkboxSelectAll"`); - await testSubjects.click('checkboxSelectAll'); - } - } + public async clickDeleteSelected() { + await this.testSubjects.click('deleteSelectedItems'); + } - /** - * Clicks NewItem button on Landing page - * @param promptBtnTestSubj testSubj locator for Prompt button - */ - public async clickNewButton(promptBtnTestSubj: string): Promise { - await retry.tryForTime(20000, async () => { - // newItemButton button is only visible when there are items in the listing table is displayed. - const isnNewItemButtonPresent = await testSubjects.exists('newItemButton', { - timeout: 10000, - }); - if (isnNewItemButtonPresent) { - await testSubjects.click('newItemButton'); - } else { - // no items exist, click createPromptButton to create new dashboard/visualization - await testSubjects.click(promptBtnTestSubj); - } - }); + public async clickItemCheckbox(id: string) { + await this.testSubjects.click(`checkboxSelectRow-${id}`); + } + + /** + * Searches for item by name, selects checbox and deletes it + * @param name item name + * @param id row id + */ + public async deleteItem(name: string, id: string) { + await this.searchForItemWithName(name); + await this.clickItemCheckbox(id); + await this.clickDeleteSelected(); + await this.common.clickConfirmOnModal(); + } + + /** + * Clicks item on Landing page by link name if it is present + */ + public async clickItemLink(appName: AppName, name: string) { + await this.testSubjects.click( + `${PREFIX_MAP[appName]}ListingTitleLink-${name.split(' ').join('-')}` + ); + } + + /** + * Checks 'SelectAll' checkbox on + */ + public async checkListingSelectAllCheckbox() { + const element = await this.testSubjects.find('checkboxSelectAll'); + const isSelected = await element.isSelected(); + if (!isSelected) { + this.log.debug(`checking checkbox "checkboxSelectAll"`); + await this.testSubjects.click('checkboxSelectAll'); } + } - public async onListingPage(appName: AppName) { - return await testSubjects.exists(`${appName}LandingPage`, { - timeout: 5000, + /** + * Clicks NewItem button on Landing page + * @param promptBtnTestSubj testSubj locator for Prompt button + */ + public async clickNewButton(promptBtnTestSubj: string): Promise { + await this.retry.tryForTime(20000, async () => { + // newItemButton button is only visible when there are items in the listing table is displayed. + const isnNewItemButtonPresent = await this.testSubjects.exists('newItemButton', { + timeout: 10000, }); - } + if (isnNewItemButtonPresent) { + await this.testSubjects.click('newItemButton'); + } else { + // no items exist, click createPromptButton to create new dashboard/visualization + await this.testSubjects.click(promptBtnTestSubj); + } + }); } - return new ListingTable(); + public async onListingPage(appName: AppName) { + return await this.testSubjects.exists(`${appName}LandingPage`, { + timeout: 5000, + }); + } } diff --git a/test/functional/services/management/index.ts b/test/functional/services/management/index.ts index 988ac06f1ca18e..8fc1a88ee5892a 100644 --- a/test/functional/services/management/index.ts +++ b/test/functional/services/management/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { ManagementMenuProvider } from './management_menu'; +export { ManagementMenuService } from './management_menu'; diff --git a/test/functional/services/management/management_menu.ts b/test/functional/services/management/management_menu.ts index eb8c6901ffa665..738a8d55439ece 100644 --- a/test/functional/services/management/management_menu.ts +++ b/test/functional/services/management/management_menu.ts @@ -6,35 +6,31 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from 'test/functional/ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; -export function ManagementMenuProvider({ getService }: FtrProviderContext) { - const find = getService('find'); +export class ManagementMenuService extends FtrService { + private readonly find = this.ctx.getService('find'); - class ManagementMenu { - public async getSections() { - const sectionsElements = await find.allByCssSelector( - '.mgtSideBarNav > .euiSideNav__content > .euiSideNavItem' - ); - - const sections = []; + public async getSections() { + const sectionsElements = await this.find.allByCssSelector( + '.mgtSideBarNav > .euiSideNav__content > .euiSideNavItem' + ); - for (const el of sectionsElements) { - const sectionId = await (await el.findByClassName('euiSideNavItemButton')).getAttribute( - 'data-test-subj' - ); - const sectionLinks = await Promise.all( - (await el.findAllByCssSelector('.euiSideNavItem > a.euiSideNavItemButton')).map((item) => - item.getAttribute('data-test-subj') - ) - ); + const sections = []; - sections.push({ sectionId, sectionLinks }); - } + for (const el of sectionsElements) { + const sectionId = await (await el.findByClassName('euiSideNavItemButton')).getAttribute( + 'data-test-subj' + ); + const sectionLinks = await Promise.all( + (await el.findAllByCssSelector('.euiSideNavItem > a.euiSideNavItemButton')).map((item) => + item.getAttribute('data-test-subj') + ) + ); - return sections; + sections.push({ sectionId, sectionLinks }); } - } - return new ManagementMenu(); + return sections; + } } diff --git a/test/functional/services/menu_toggle.ts b/test/functional/services/menu_toggle.ts index 866d73bd9df251..4de66a5697775d 100644 --- a/test/functional/services/menu_toggle.ts +++ b/test/functional/services/menu_toggle.ts @@ -6,61 +6,56 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function MenuToggleProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const testSubjects = getService('testSubjects'); - - interface Options { - name: string; - menuTestSubject: string; - toggleButtonTestSubject: string; - } - - return class MenuToggle { - private readonly name: string; - private readonly menuTestSubject: string; - private readonly toggleButtonTestSubject: string; +interface Options { + name: string; + menuTestSubject: string; + toggleButtonTestSubject: string; +} - constructor(options: Options) { - this.name = options.name; - this.menuTestSubject = options.menuTestSubject; - this.toggleButtonTestSubject = options.toggleButtonTestSubject; - } +export class MenuToggleService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly testSubjects = this.ctx.getService('testSubjects'); - async open() { - await this.setState(true); - } + create(options: Options) { + const { log, retry, testSubjects } = this; + const { name, menuTestSubject, toggleButtonTestSubject } = options; - async close() { - await this.setState(false); - } - - private async setState(expectedState: boolean) { + async function setState(expectedState: boolean) { log.debug( - `setting menu open state [name=${this.name}] [state=${expectedState ? 'open' : 'closed'}]` + `setting menu open state [name=${name}] [state=${expectedState ? 'open' : 'closed'}]` ); await retry.try(async () => { // if the menu is clearly in the expected state already, bail out quickly if so - const isOpen = await testSubjects.exists(this.menuTestSubject, { timeout: 1000 }); + const isOpen = await testSubjects.exists(menuTestSubject, { timeout: 1000 }); if (isOpen === expectedState) { return; } // toggle the view state by clicking the button - await testSubjects.click(this.toggleButtonTestSubject); + await testSubjects.click(toggleButtonTestSubject); if (expectedState === true) { // wait for up to 10 seconds for the menu to show up, otherwise fail and retry - await testSubjects.existOrFail(this.menuTestSubject, { timeout: 10000 }); + await testSubjects.existOrFail(menuTestSubject, { timeout: 10000 }); } else { // wait for the form to hide, otherwise fail and retry - await testSubjects.waitForDeleted(this.menuTestSubject); + await testSubjects.waitForDeleted(menuTestSubject); } }); } - }; + + return { + async open() { + await setState(true); + }, + + async close() { + await setState(false); + }, + }; + } } diff --git a/test/functional/services/query_bar.ts b/test/functional/services/query_bar.ts index 2c4cd3b8db131f..31586d92d92a9d 100644 --- a/test/functional/services/query_bar.ts +++ b/test/functional/services/query_bar.ts @@ -7,82 +7,76 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function QueryBarProvider({ getService, getPageObjects }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const retry = getService('retry'); - const log = getService('log'); - const PageObjects = getPageObjects(['header', 'common']); - const find = getService('find'); - const browser = getService('browser'); +export class QueryBarService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly retry = this.ctx.getService('retry'); + private readonly log = this.ctx.getService('log'); + private readonly PageObjects = this.ctx.getPageObjects(['header', 'common']); + private readonly find = this.ctx.getService('find'); + private readonly browser = this.ctx.getService('browser'); - class QueryBar { - async getQueryString(): Promise { - return await testSubjects.getAttribute('queryInput', 'value'); - } + async getQueryString(): Promise { + return await this.testSubjects.getAttribute('queryInput', 'value'); + } - public async setQuery(query: string): Promise { - log.debug(`QueryBar.setQuery(${query})`); - // Extra caution used because of flaky test here: https://github.com/elastic/kibana/issues/16978 doesn't seem - // to be actually setting the query in the query input based off - await retry.try(async () => { - await testSubjects.click('queryInput'); + public async setQuery(query: string): Promise { + this.log.debug(`QueryBar.setQuery(${query})`); + // Extra caution used because of flaky test here: https://github.com/elastic/kibana/issues/16978 doesn't seem + // to be actually setting the query in the query input based off + await this.retry.try(async () => { + await this.testSubjects.click('queryInput'); - // testSubjects.setValue uses input.clearValue which wasn't working, but input.clearValueWithKeyboard does. - // So the following lines do the same thing as input.setValue but with input.clearValueWithKeyboard instead. - const input = await find.activeElement(); - await input.clearValueWithKeyboard(); - await input.type(query); - const currentQuery = await this.getQueryString(); - if (currentQuery !== query) { - throw new Error( - `Failed to set query input to ${query}, instead query is ${currentQuery}` - ); - } - }); - } - - public async clearQuery(): Promise { - await this.setQuery(''); - await PageObjects.common.pressTabKey(); // move outside of input into language switcher - await PageObjects.common.pressTabKey(); // move outside of language switcher so time picker appears - } + // this.testSubjects.setValue uses input.clearValue which wasn't working, but input.clearValueWithKeyboard does. + // So the following lines do the same thing as input.setValue but with input.clearValueWithKeyboard instead. + const input = await this.find.activeElement(); + await input.clearValueWithKeyboard(); + await input.type(query); + const currentQuery = await this.getQueryString(); + if (currentQuery !== query) { + throw new Error(`Failed to set query input to ${query}, instead query is ${currentQuery}`); + } + }); + } - public async submitQuery(): Promise { - log.debug('QueryBar.submitQuery'); - await testSubjects.click('queryInput'); - await PageObjects.common.pressEnterKey(); - await PageObjects.header.waitUntilLoadingHasFinished(); - } + public async clearQuery(): Promise { + await this.setQuery(''); + await this.PageObjects.common.pressTabKey(); // move outside of input into language switcher + await this.PageObjects.common.pressTabKey(); // move outside of language switcher so time picker appears + } - public async clickQuerySubmitButton(): Promise { - await testSubjects.click('querySubmitButton'); - } + public async submitQuery(): Promise { + this.log.debug('QueryBar.submitQuery'); + await this.testSubjects.click('queryInput'); + await this.PageObjects.common.pressEnterKey(); + await this.PageObjects.header.waitUntilLoadingHasFinished(); + } - public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { - await testSubjects.click('switchQueryLanguageButton'); - const kqlToggle = await testSubjects.find('languageToggle'); - const currentLang = - (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; - if (lang !== currentLang) { - await kqlToggle.click(); - } + public async clickQuerySubmitButton(): Promise { + await this.testSubjects.click('querySubmitButton'); + } - await browser.pressKeys(browser.keys.ESCAPE); // close popover - await this.expectQueryLanguageOrFail(lang); // make sure lang is switched + public async switchQueryLanguage(lang: 'kql' | 'lucene'): Promise { + await this.testSubjects.click('switchQueryLanguageButton'); + const kqlToggle = await this.testSubjects.find('languageToggle'); + const currentLang = + (await kqlToggle.getAttribute('aria-checked')) === 'true' ? 'kql' : 'lucene'; + if (lang !== currentLang) { + await kqlToggle.click(); } - public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { - const queryLanguageButton = await testSubjects.find('switchQueryLanguageButton'); - expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); - } + await this.browser.pressKeys(this.browser.keys.ESCAPE); // close popover + await this.expectQueryLanguageOrFail(lang); // make sure lang is switched + } - public async getSuggestions() { - const suggestions = await testSubjects.findAll('autoCompleteSuggestionText'); - return Promise.all(suggestions.map((suggestion) => suggestion.getVisibleText())); - } + public async expectQueryLanguageOrFail(lang: 'kql' | 'lucene'): Promise { + const queryLanguageButton = await this.testSubjects.find('switchQueryLanguageButton'); + expect((await queryLanguageButton.getVisibleText()).toLowerCase()).to.eql(lang); } - return new QueryBar(); + public async getSuggestions() { + const suggestions = await this.testSubjects.findAll('autoCompleteSuggestionText'); + return Promise.all(suggestions.map((suggestion) => suggestion.getVisibleText())); + } } diff --git a/test/functional/services/renderable.ts b/test/functional/services/renderable.ts index 42ea808bd64ba4..da298b6ec0343e 100644 --- a/test/functional/services/renderable.ts +++ b/test/functional/services/renderable.ts @@ -6,49 +6,45 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; const RENDER_COMPLETE_SELECTOR = '[data-render-complete="true"]'; const RENDER_COMPLETE_PENDING_SELECTOR = '[data-render-complete="false"]'; const DATA_LOADING_SELECTOR = '[data-loading]'; -export function RenderableProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const find = getService('find'); +export class RenderableService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly find = this.ctx.getService('find'); - class Renderable { - /** - * This method waits for a certain number of objects to finish rendering and loading, which is indicated - * by a couple tags. The RENDER_COMPLETE_SELECTOR indicates that it's done initially loading up. Some - * visualizations also add a DATA_LOADING_SELECTOR when the internal data is loading. This test will not - * return if any of those tags are found. - * @param count {Number} Number of RENDER_COMPLETE_SELECTORs to wait for. - */ - public async waitForRender(count: number = 1): Promise { - log.debug(`Renderable.waitForRender for ${count} elements`); - await retry.try(async () => { - const completedElements = await find.allByCssSelector(RENDER_COMPLETE_SELECTOR); - if (completedElements.length < count) { - const pendingElements = await find.allByCssSelector(RENDER_COMPLETE_PENDING_SELECTOR); - const pendingElementNames = []; - for (const pendingElement of pendingElements) { - const title = await pendingElement.getAttribute('data-title'); - pendingElementNames.push(title); - } - throw new Error(`${ - completedElements.length - } elements completed rendering, still waiting on a total of ${count} - specifically:\n${pendingElementNames.join('\n')}`); + /** + * This method waits for a certain number of objects to finish rendering and loading, which is indicated + * by a couple tags. The RENDER_COMPLETE_SELECTOR indicates that it's done initially loading up. Some + * visualizations also add a DATA_LOADING_SELECTOR when the internal data is loading. This test will not + * return if any of those tags are found. + * @param count {Number} Number of RENDER_COMPLETE_SELECTORs to wait for. + */ + public async waitForRender(count: number = 1): Promise { + this.log.debug(`Renderable.waitForRender for ${count} elements`); + await this.retry.try(async () => { + const completedElements = await this.find.allByCssSelector(RENDER_COMPLETE_SELECTOR); + if (completedElements.length < count) { + const pendingElements = await this.find.allByCssSelector(RENDER_COMPLETE_PENDING_SELECTOR); + const pendingElementNames = []; + for (const pendingElement of pendingElements) { + const title = await pendingElement.getAttribute('data-title'); + pendingElementNames.push(title); } + throw new Error(`${ + completedElements.length + } elements completed rendering, still waiting on a total of ${count} + specifically:\n${pendingElementNames.join('\n')}`); + } - const stillLoadingElements = await find.allByCssSelector(DATA_LOADING_SELECTOR, 1000); - if (stillLoadingElements.length > 0) { - throw new Error(`${stillLoadingElements.length} elements still loading contents`); - } - }); - } + const stillLoadingElements = await this.find.allByCssSelector(DATA_LOADING_SELECTOR, 1000); + if (stillLoadingElements.length > 0) { + throw new Error(`${stillLoadingElements.length} elements still loading contents`); + } + }); } - - return new Renderable(); } diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index d4fa34f224547c..aabe8c0aebb0c6 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -7,210 +7,205 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../ftr_provider_context'; - -export function SavedQueryManagementComponentProvider({ - getService, - getPageObjects, -}: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const queryBar = getService('queryBar'); - const retry = getService('retry'); - const config = getService('config'); - const PageObjects = getPageObjects(['common']); - - class SavedQueryManagementComponent { - public async getCurrentlyLoadedQueryID() { - await this.openSavedQueryManagementComponent(); - try { - return await testSubjects.getVisibleText('~saved-query-list-item-selected'); - } catch { - return undefined; - } - } - - public async saveNewQuery( - name: string, - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - await this.openSaveCurrentQueryModal(); - await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); +import { FtrService } from '../ftr_provider_context'; + +export class SavedQueryManagementComponentService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly queryBar = this.ctx.getService('queryBar'); + private readonly retry = this.ctx.getService('retry'); + private readonly config = this.ctx.getService('config'); + private readonly PageObjects = this.ctx.getPageObjects(['common']); + + public async getCurrentlyLoadedQueryID() { + await this.openSavedQueryManagementComponent(); + try { + return await this.testSubjects.getVisibleText('~saved-query-list-item-selected'); + } catch { + return undefined; } + } - public async saveNewQueryWithNameError(name?: string) { - await this.openSaveCurrentQueryModal(); - if (name) { - await testSubjects.setValue('saveQueryFormTitle', name); - } - - // Form input validation only happens onBlur. Clicking the save button should de-focus the - // input element and the validation should prevent a save from actually happening if there's - // an error. - await testSubjects.click('savedQueryFormSaveButton'); - - await retry.waitForWithTimeout('save button to be disabled', 1000, async () => { - const saveQueryFormSaveButtonStatus = await testSubjects.isEnabled( - 'savedQueryFormSaveButton' - ); - return saveQueryFormSaveButtonStatus === false; - }); + public async saveNewQuery( + name: string, + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + await this.openSaveCurrentQueryModal(); + await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); + } - await testSubjects.click('savedQueryFormCancelButton'); + public async saveNewQueryWithNameError(name?: string) { + await this.openSaveCurrentQueryModal(); + if (name) { + await this.testSubjects.setValue('saveQueryFormTitle', name); } - public async saveCurrentlyLoadedAsNewQuery( - name: string, - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-save-as-new-button'); - await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); - } + // Form input validation only happens onBlur. Clicking the save button should de-focus the + // input element and the validation should prevent a save from actually happening if there's + // an error. + await this.testSubjects.click('savedQueryFormSaveButton'); - public async updateCurrentlyLoadedQuery( - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-save-changes-button'); - await this.submitSaveQueryForm(null, description, includeFilters, includeTimeFilter); - } + await this.retry.waitForWithTimeout('save button to be disabled', 1000, async () => { + const saveQueryFormSaveButtonStatus = await this.testSubjects.isEnabled( + 'savedQueryFormSaveButton' + ); + return saveQueryFormSaveButtonStatus === false; + }); - public async loadSavedQuery(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click(`~load-saved-query-${title}-button`); - await retry.try(async () => { - await this.openSavedQueryManagementComponent(); - const selectedSavedQueryText = await testSubjects.getVisibleText( - '~saved-query-list-item-selected' - ); - expect(selectedSavedQueryText).to.eql(title); - }); - await this.closeSavedQueryManagementComponent(); - } + await this.testSubjects.click('savedQueryFormCancelButton'); + } - public async deleteSavedQuery(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.click(`~delete-saved-query-${title}-button`); - await PageObjects.common.clickConfirmOnModal(); - } + public async saveCurrentlyLoadedAsNewQuery( + name: string, + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-save-as-new-button'); + await this.submitSaveQueryForm(name, description, includeFilters, includeTimeFilter); + } - async clearCurrentlyLoadedQuery() { + public async updateCurrentlyLoadedQuery( + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-save-changes-button'); + await this.submitSaveQueryForm(null, description, includeFilters, includeTimeFilter); + } + + public async loadSavedQuery(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click(`~load-saved-query-${title}-button`); + await this.retry.try(async () => { await this.openSavedQueryManagementComponent(); - await testSubjects.click('saved-query-management-clear-button'); - await this.closeSavedQueryManagementComponent(); - const queryString = await queryBar.getQueryString(); - expect(queryString).to.be.empty(); - } + const selectedSavedQueryText = await this.testSubjects.getVisibleText( + '~saved-query-list-item-selected' + ); + expect(selectedSavedQueryText).to.eql(title); + }); + await this.closeSavedQueryManagementComponent(); + } - async submitSaveQueryForm( - title: string | null, - description: string, - includeFilters: boolean, - includeTimeFilter: boolean - ) { - if (title) { - await testSubjects.setValue('saveQueryFormTitle', title); - } - await testSubjects.setValue('saveQueryFormDescription', description); - - const currentIncludeFiltersValue = - (await testSubjects.getAttribute('saveQueryFormIncludeFiltersOption', 'aria-checked')) === - 'true'; - if (currentIncludeFiltersValue !== includeFilters) { - await testSubjects.click('saveQueryFormIncludeFiltersOption'); - } - - const currentIncludeTimeFilterValue = - (await testSubjects.getAttribute( - 'saveQueryFormIncludeTimeFilterOption', - 'aria-checked' - )) === 'true'; - if (currentIncludeTimeFilterValue !== includeTimeFilter) { - await testSubjects.click('saveQueryFormIncludeTimeFilterOption'); - } - - await testSubjects.click('savedQueryFormSaveButton'); - } + public async deleteSavedQuery(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click(`~delete-saved-query-${title}-button`); + await this.PageObjects.common.clickConfirmOnModal(); + } - async savedQueryExist(title: string) { - await this.openSavedQueryManagementComponent(); - const exists = testSubjects.exists(`~load-saved-query-${title}-button`); - await this.closeSavedQueryManagementComponent(); - return exists; - } + async clearCurrentlyLoadedQuery() { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.click('saved-query-management-clear-button'); + await this.closeSavedQueryManagementComponent(); + const queryString = await this.queryBar.getQueryString(); + expect(queryString).to.be.empty(); + } - async savedQueryExistOrFail(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.existOrFail(`~load-saved-query-${title}-button`); + async submitSaveQueryForm( + title: string | null, + description: string, + includeFilters: boolean, + includeTimeFilter: boolean + ) { + if (title) { + await this.testSubjects.setValue('saveQueryFormTitle', title); } + await this.testSubjects.setValue('saveQueryFormDescription', description); - async savedQueryTextExist(text: string) { - await this.openSavedQueryManagementComponent(); - const queryString = await queryBar.getQueryString(); - expect(queryString).to.eql(text); + const currentIncludeFiltersValue = + (await this.testSubjects.getAttribute( + 'saveQueryFormIncludeFiltersOption', + 'aria-checked' + )) === 'true'; + if (currentIncludeFiltersValue !== includeFilters) { + await this.testSubjects.click('saveQueryFormIncludeFiltersOption'); } - async savedQueryMissingOrFail(title: string) { - await retry.try(async () => { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail(`~load-saved-query-${title}-button`); - }); - await this.closeSavedQueryManagementComponent(); + const currentIncludeTimeFilterValue = + (await this.testSubjects.getAttribute( + 'saveQueryFormIncludeTimeFilterOption', + 'aria-checked' + )) === 'true'; + if (currentIncludeTimeFilterValue !== includeTimeFilter) { + await this.testSubjects.click('saveQueryFormIncludeTimeFilterOption'); } - async openSavedQueryManagementComponent() { - const isOpenAlready = await testSubjects.exists('saved-query-management-popover'); - if (isOpenAlready) return; + await this.testSubjects.click('savedQueryFormSaveButton'); + } - await retry.waitFor('saved query management popover to have any text', async () => { - await testSubjects.click('saved-query-management-popover-button'); - const queryText = await testSubjects.getVisibleText('saved-query-management-popover'); - return queryText.length > 0; - }); - } + async savedQueryExist(title: string) { + await this.openSavedQueryManagementComponent(); + const exists = this.testSubjects.exists(`~load-saved-query-${title}-button`); + await this.closeSavedQueryManagementComponent(); + return exists; + } - async closeSavedQueryManagementComponent() { - const isOpenAlready = await testSubjects.exists('saved-query-management-popover'); - if (!isOpenAlready) return; + async savedQueryExistOrFail(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.existOrFail(`~load-saved-query-${title}-button`); + } - await retry.try(async () => { - await testSubjects.click('saved-query-management-popover-button'); - await testSubjects.missingOrFail('saved-query-management-popover'); - }); - } + async savedQueryTextExist(text: string) { + await this.openSavedQueryManagementComponent(); + const queryString = await this.queryBar.getQueryString(); + expect(queryString).to.eql(text); + } - async openSaveCurrentQueryModal() { + async savedQueryMissingOrFail(title: string) { + await this.retry.try(async () => { await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail(`~load-saved-query-${title}-button`); + }); + await this.closeSavedQueryManagementComponent(); + } - await retry.try(async () => { - await testSubjects.click('saved-query-management-save-button'); - await testSubjects.existOrFail('saveQueryForm', { - timeout: config.get('timeouts.waitForExists'), - }); - }); - } + async openSavedQueryManagementComponent() { + const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + if (isOpenAlready) return; - async saveNewQueryMissingOrFail() { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail('saved-query-management-save-button'); - } + await this.retry.waitFor('saved query management popover to have any text', async () => { + await this.testSubjects.click('saved-query-management-popover-button'); + const queryText = await this.testSubjects.getVisibleText('saved-query-management-popover'); + return queryText.length > 0; + }); + } - async updateCurrentlyLoadedQueryMissingOrFail() { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail('saved-query-management-save-changes-button'); - } + async closeSavedQueryManagementComponent() { + const isOpenAlready = await this.testSubjects.exists('saved-query-management-popover'); + if (!isOpenAlready) return; - async deleteSavedQueryMissingOrFail(title: string) { - await this.openSavedQueryManagementComponent(); - await testSubjects.missingOrFail(`delete-saved-query-${title}-button`); - } + await this.retry.try(async () => { + await this.testSubjects.click('saved-query-management-popover-button'); + await this.testSubjects.missingOrFail('saved-query-management-popover'); + }); + } + + async openSaveCurrentQueryModal() { + await this.openSavedQueryManagementComponent(); + + await this.retry.try(async () => { + await this.testSubjects.click('saved-query-management-save-button'); + await this.testSubjects.existOrFail('saveQueryForm', { + timeout: this.config.get('timeouts.waitForExists'), + }); + }); + } + + async saveNewQueryMissingOrFail() { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail('saved-query-management-save-button'); } - return new SavedQueryManagementComponent(); + async updateCurrentlyLoadedQueryMissingOrFail() { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail('saved-query-management-save-changes-button'); + } + + async deleteSavedQueryMissingOrFail(title: string) { + await this.openSavedQueryManagementComponent(); + await this.testSubjects.missingOrFail(`delete-saved-query-${title}-button`); + } } diff --git a/test/functional/services/toasts.ts b/test/functional/services/toasts.ts index aeaf79e75574a1..d71d66e399785e 100644 --- a/test/functional/services/toasts.ts +++ b/test/functional/services/toasts.ts @@ -6,78 +6,74 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { FtrService } from '../ftr_provider_context'; -export function ToastsProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); +export class ToastsService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); - class Toasts { - /** - * Returns the title and message of a specific error toast. - * This method is specific to toasts created via `.addError` since they contain - * an additional button, that should not be part of the message. - * - * @param index The index of the toast (1-based, NOT 0-based!) of the toast. Use first by default. - * @returns The title and message of the specified error toast.https://github.com/elastic/kibana/issues/17087 - */ - public async getErrorToast(index: number = 1) { - const toast = await this.getToastElement(index); - const titleElement = await testSubjects.findDescendant('euiToastHeader', toast); - const title: string = await titleElement.getVisibleText(); - const messageElement = await testSubjects.findDescendant('errorToastMessage', toast); - const message: string = await messageElement.getVisibleText(); - return { title, message }; - } + /** + * Returns the title and message of a specific error toast. + * This method is specific to toasts created via `.addError` since they contain + * an additional button, that should not be part of the message. + * + * @param index The index of the toast (1-based, NOT 0-based!) of the toast. Use first by default. + * @returns The title and message of the specified error toast.https://github.com/elastic/kibana/issues/17087 + */ + public async getErrorToast(index: number = 1) { + const toast = await this.getToastElement(index); + const titleElement = await this.testSubjects.findDescendant('euiToastHeader', toast); + const title: string = await titleElement.getVisibleText(); + const messageElement = await this.testSubjects.findDescendant('errorToastMessage', toast); + const message: string = await messageElement.getVisibleText(); + return { title, message }; + } - /** - * Dismiss a specific toast from the toast list. Since toasts usually should time out themselves, - * you only need to call this for permanent toasts (e.g. error toasts). - * - * @param index The 1-based index of the toast to dismiss. Use first by default. - */ - public async dismissToast(index: number = 1) { - const toast = await this.getToastElement(index); - await toast.moveMouseTo(); - const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); - await dismissButton.click(); - } + /** + * Dismiss a specific toast from the toast list. Since toasts usually should time out themselves, + * you only need to call this for permanent toasts (e.g. error toasts). + * + * @param index The 1-based index of the toast to dismiss. Use first by default. + */ + public async dismissToast(index: number = 1) { + const toast = await this.getToastElement(index); + await toast.moveMouseTo(); + const dismissButton = await this.testSubjects.findDescendant('toastCloseButton', toast); + await dismissButton.click(); + } - public async dismissAllToasts() { - const list = await this.getGlobalToastList(); - const toasts = await list.findAllByCssSelector(`.euiToast`); + public async dismissAllToasts() { + const list = await this.getGlobalToastList(); + const toasts = await list.findAllByCssSelector(`.euiToast`); - if (toasts.length === 0) return; + if (toasts.length === 0) return; - for (const toast of toasts) { - await toast.moveMouseTo(); + for (const toast of toasts) { + await toast.moveMouseTo(); - if (await testSubjects.descendantExists('toastCloseButton', toast)) { - try { - const dismissButton = await testSubjects.findDescendant('toastCloseButton', toast); - await dismissButton.click(); - } catch (err) { - // ignore errors - // toasts are finnicky because they can dismiss themselves right before you close them - } + if (await this.testSubjects.descendantExists('toastCloseButton', toast)) { + try { + const dismissButton = await this.testSubjects.findDescendant('toastCloseButton', toast); + await dismissButton.click(); + } catch (err) { + // ignore errors + // toasts are finnicky because they can dismiss themselves right before you close them } } } + } - public async getToastElement(index: number) { - const list = await this.getGlobalToastList(); - return await list.findByCssSelector(`.euiToast:nth-child(${index})`); - } - - private async getGlobalToastList() { - return await testSubjects.find('globalToastList'); - } + public async getToastElement(index: number) { + const list = await this.getGlobalToastList(); + return await list.findByCssSelector(`.euiToast:nth-child(${index})`); + } - public async getToastCount() { - const list = await this.getGlobalToastList(); - const toasts = await list.findAllByCssSelector(`.euiToast`); - return toasts.length; - } + private async getGlobalToastList() { + return await this.testSubjects.find('globalToastList'); } - return new Toasts(); + public async getToastCount() { + const list = await this.getGlobalToastList(); + const toasts = await list.findAllByCssSelector(`.euiToast`); + return toasts.length; + } } diff --git a/test/functional/services/visualizations/elastic_chart.ts b/test/functional/services/visualizations/elastic_chart.ts index 80483100a06dd3..b954b4ba03616e 100644 --- a/test/functional/services/visualizations/elastic_chart.ts +++ b/test/functional/services/visualizations/elastic_chart.ts @@ -9,7 +9,7 @@ import { DebugState } from '@elastic/charts'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; import { WebElementWrapper } from '../lib/web_element_wrapper'; declare global { @@ -21,115 +21,111 @@ declare global { } } -export function ElasticChartProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const retry = getService('retry'); - const log = getService('log'); - const browser = getService('browser'); - - class ElasticChart { - public async getCanvas(dataTestSubj?: string) { - if (dataTestSubj) { - const chart = await this.getChart(dataTestSubj); - return await chart.findByClassName('echCanvasRenderer'); - } - return await find.byCssSelector('.echChart canvas:last-of-type'); - } +export class ElasticChartService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly find = this.ctx.getService('find'); + private readonly retry = this.ctx.getService('retry'); + private readonly log = this.ctx.getService('log'); + private readonly browser = this.ctx.getService('browser'); - public async canvasExists() { - return await find.existsByCssSelector('.echChart canvas:last-of-type'); - } - - public async waitForRenderComplete(dataTestSubj?: string) { + public async getCanvas(dataTestSubj?: string) { + if (dataTestSubj) { const chart = await this.getChart(dataTestSubj); - const rendered = await chart.findAllByCssSelector( - '.echChartStatus[data-ech-render-complete=true]' - ); - expect(rendered.length).to.equal(1); + return await chart.findByClassName('echCanvasRenderer'); } + return await this.find.byCssSelector('.echChart canvas:last-of-type'); + } - public async getVisualizationRenderingCount(dataTestSubj?: string) { - const chart = await this.getChart(dataTestSubj); - const visContainer = await chart.findByCssSelector('.echChartStatus'); - const renderingCount = await visContainer.getAttribute('data-ech-render-count'); - return Number(renderingCount); - } + public async canvasExists() { + return await this.find.existsByCssSelector('.echChart canvas:last-of-type'); + } - public async waitForRenderingCount(minimumCount: number, dataTestSubj?: string) { - await retry.waitFor(`rendering count to be equal to [${minimumCount}]`, async () => { - const currentRenderingCount = await this.getVisualizationRenderingCount(dataTestSubj); - log.debug(`-- currentRenderingCount=${currentRenderingCount}`); - return currentRenderingCount >= minimumCount; - }); - } + public async waitForRenderComplete(dataTestSubj?: string) { + const chart = await this.getChart(dataTestSubj); + const rendered = await chart.findAllByCssSelector( + '.echChartStatus[data-ech-render-complete=true]' + ); + expect(rendered.length).to.equal(1); + } - public async hasChart(dataTestSubj?: string, timeout?: number): Promise { - if (dataTestSubj) { - return await testSubjects.exists(dataTestSubj, { timeout }); - } else { - const charts = await this.getAllCharts(timeout); + public async getVisualizationRenderingCount(dataTestSubj?: string) { + const chart = await this.getChart(dataTestSubj); + const visContainer = await chart.findByCssSelector('.echChartStatus'); + const renderingCount = await visContainer.getAttribute('data-ech-render-count'); + return Number(renderingCount); + } - return charts.length > 0; - } - } + public async waitForRenderingCount(minimumCount: number, dataTestSubj?: string) { + await this.retry.waitFor(`rendering count to be equal to [${minimumCount}]`, async () => { + const currentRenderingCount = await this.getVisualizationRenderingCount(dataTestSubj); + this.log.debug(`-- currentRenderingCount=${currentRenderingCount}`); + return currentRenderingCount >= minimumCount; + }); + } - private async getChart( - dataTestSubj?: string, - timeout?: number, - match: number = 0 - ): Promise { - if (dataTestSubj) { - if (!(await testSubjects.exists(dataTestSubj, { timeout }))) { - throw Error(`Failed to find an elastic-chart with testSubject '${dataTestSubj}'`); - } - - return (await testSubjects.findAll(dataTestSubj))[match]; - } else { - const charts = await this.getAllCharts(timeout); - if (charts.length === 0) { - throw Error(`Failed to find any elastic-charts on the page`); - } else { - return charts[match]; - } - } - } + public async hasChart(dataTestSubj?: string, timeout?: number): Promise { + if (dataTestSubj) { + return await this.testSubjects.exists(dataTestSubj, { timeout }); + } else { + const charts = await this.getAllCharts(timeout); - private async getAllCharts(timeout?: number) { - return await find.allByCssSelector('.echChart', timeout); + return charts.length > 0; } + } - /** - * used to get chart data from `@elastic/charts` - * requires `window._echDebugStateFlag` to be true - */ - public async getChartDebugData( - dataTestSubj?: string, - match: number = 0 - ): Promise { - const chart = await this.getChart(dataTestSubj, undefined, match); - - try { - const visContainer = await chart.findByCssSelector('.echChartStatus'); - const debugDataString: string | undefined = await visContainer.getAttribute( - 'data-ech-debug-state' - ); - return debugDataString ? JSON.parse(debugDataString) : null; - } catch (error) { - throw Error('Elastic charts debugState not found'); + private async getChart( + dataTestSubj?: string, + timeout?: number, + match: number = 0 + ): Promise { + if (dataTestSubj) { + if (!(await this.testSubjects.exists(dataTestSubj, { timeout }))) { + throw Error(`Failed to find an elastic-chart with testSubject '${dataTestSubj}'`); + } + + return (await this.testSubjects.findAll(dataTestSubj))[match]; + } else { + const charts = await this.getAllCharts(timeout); + if (charts.length === 0) { + throw Error(`Failed to find any elastic-charts on the page`); + } else { + return charts[match]; } } + } - /** - * Used to set a flag on the window to trigger debug state on elastic charts - * @param value - */ - public async setNewChartUiDebugFlag(value = true) { - await browser.execute<[boolean], void>((v) => { - window._echDebugStateFlag = v; - }, value); + private async getAllCharts(timeout?: number) { + return await this.find.allByCssSelector('.echChart', timeout); + } + + /** + * used to get chart data from `@elastic/charts` + * requires `window._echDebugStateFlag` to be true + */ + public async getChartDebugData( + dataTestSubj?: string, + match: number = 0 + ): Promise { + const chart = await this.getChart(dataTestSubj, undefined, match); + + try { + const visContainer = await chart.findByCssSelector('.echChartStatus'); + const debugDataString: string | undefined = await visContainer.getAttribute( + 'data-ech-debug-state' + ); + return debugDataString ? JSON.parse(debugDataString) : null; + } catch (error) { + throw Error('Elastic charts debugState not found'); } } - return new ElasticChart(); + /** + * Used to set a flag on the window to trigger debug state on elastic charts + * @param value + */ + public async setNewChartUiDebugFlag(value = true) { + await this.browser.execute<[boolean], void>((v) => { + window._echDebugStateFlag = v; + }, value); + } } diff --git a/test/functional/services/visualizations/index.ts b/test/functional/services/visualizations/index.ts index 9c43a1be83de85..22c06f3821a309 100644 --- a/test/functional/services/visualizations/index.ts +++ b/test/functional/services/visualizations/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export { PieChartProvider } from './pie_chart'; -export { ElasticChartProvider } from './elastic_chart'; -export { VegaDebugInspectorViewProvider } from './vega_debug_inspector'; +export { PieChartService } from './pie_chart'; +export { ElasticChartService } from './elastic_chart'; +export { VegaDebugInspectorViewService } from './vega_debug_inspector'; diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index bdf26ab8a5b448..cac4e8fe64c5e2 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -7,111 +7,111 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export function PieChartProvider({ getService }: FtrProviderContext) { - const log = getService('log'); - const retry = getService('retry'); - const config = getService('config'); - const inspector = getService('inspector'); - const testSubjects = getService('testSubjects'); - const find = getService('find'); - const defaultFindTimeout = config.get('timeouts.find'); - const panelActions = getService('dashboardPanelActions'); - - return new (class PieChart { - private readonly filterActionText = 'Apply filter to current view'; - - async clickOnPieSlice(name?: string) { - log.debug(`PieChart.clickOnPieSlice(${name})`); - if (name) { - await testSubjects.click(`pieSlice-${name.split(' ').join('-')}`); - } else { - // If no pie slice has been provided, find the first one available. - await retry.try(async () => { - const slices = await find.allByCssSelector('svg > g > g.arcs > path.slice'); - log.debug('Slices found:' + slices.length); - return slices[0].click(); - }); - } - } - - async filterOnPieSlice(name?: string) { - log.debug(`PieChart.filterOnPieSlice(${name})`); - await this.clickOnPieSlice(name); - const hasUiActionsPopup = await testSubjects.exists('multipleActionsContextMenu'); - if (hasUiActionsPopup) { - const actionElement = await panelActions.getActionWebElementByText(this.filterActionText); - await actionElement.click(); - } - } - - async filterByLegendItem(label: string) { - log.debug(`PieChart.filterByLegendItem(${label})`); - await testSubjects.click(`legend-${label}`); - await testSubjects.click(`legend-${label}-filterIn`); - } - - async getPieSlice(name: string) { - return await testSubjects.find(`pieSlice-${name.split(' ').join('-')}`); - } - - async getAllPieSlices(name: string) { - return await testSubjects.findAll(`pieSlice-${name.split(' ').join('-')}`); - } - - async getPieSliceStyle(name: string) { - log.debug(`VisualizePage.getPieSliceStyle(${name})`); - const pieSlice = await this.getPieSlice(name); - return await pieSlice.getAttribute('style'); - } - - async getAllPieSliceStyles(name: string) { - log.debug(`VisualizePage.getAllPieSliceStyles(${name})`); - const pieSlices = await this.getAllPieSlices(name); - return await Promise.all( - pieSlices.map(async (pieSlice) => await pieSlice.getAttribute('style')) - ); - } - - async getPieChartData() { - const chartTypes = await find.allByCssSelector('path.slice', defaultFindTimeout * 2); - return await Promise.all(chartTypes.map(async (chart) => await chart.getAttribute('d'))); - } - - async expectPieChartTableData(expectedTableData: Array<[]>) { - await inspector.open(); - await inspector.setTablePageSize(50); - await inspector.expectTableData(expectedTableData); - } - - async getPieChartLabels() { - const chartTypes = await find.allByCssSelector('path.slice', defaultFindTimeout * 2); - return await Promise.all( - chartTypes.map(async (chart) => await chart.getAttribute('data-label')) - ); - } - - async getPieSliceCount() { - log.debug('PieChart.getPieSliceCount'); - const slices = await find.allByCssSelector('svg > g > g.arcs > path.slice'); - return slices.length; - } - - async expectPieSliceCount(expectedCount: number) { - log.debug(`PieChart.expectPieSliceCount(${expectedCount})`); - await retry.try(async () => { - const slicesCount = await this.getPieSliceCount(); - expect(slicesCount).to.be(expectedCount); +import { FtrService } from '../../ftr_provider_context'; + +export class PieChartService extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly retry = this.ctx.getService('retry'); + private readonly config = this.ctx.getService('config'); + private readonly inspector = this.ctx.getService('inspector'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly find = this.ctx.getService('find'); + private readonly panelActions = this.ctx.getService('dashboardPanelActions'); + private readonly defaultFindTimeout = this.config.get('timeouts.find'); + + private readonly filterActionText = 'Apply filter to current view'; + + async clickOnPieSlice(name?: string) { + this.log.debug(`PieChart.clickOnPieSlice(${name})`); + if (name) { + await this.testSubjects.click(`pieSlice-${name.split(' ').join('-')}`); + } else { + // If no pie slice has been provided, find the first one available. + await this.retry.try(async () => { + const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice'); + this.log.debug('Slices found:' + slices.length); + return slices[0].click(); }); } - - async expectPieChartLabels(expectedLabels: string[]) { - log.debug(`PieChart.expectPieChartLabels(${expectedLabels.join(',')})`); - await retry.try(async () => { - const pieData = await this.getPieChartLabels(); - expect(pieData).to.eql(expectedLabels); - }); + } + + async filterOnPieSlice(name?: string) { + this.log.debug(`PieChart.filterOnPieSlice(${name})`); + await this.clickOnPieSlice(name); + const hasUiActionsPopup = await this.testSubjects.exists('multipleActionsContextMenu'); + if (hasUiActionsPopup) { + const actionElement = await this.panelActions.getActionWebElementByText( + this.filterActionText + ); + await actionElement.click(); } - })(); + } + + async filterByLegendItem(label: string) { + this.log.debug(`PieChart.filterByLegendItem(${label})`); + await this.testSubjects.click(`legend-${label}`); + await this.testSubjects.click(`legend-${label}-filterIn`); + } + + async getPieSlice(name: string) { + return await this.testSubjects.find(`pieSlice-${name.split(' ').join('-')}`); + } + + async getAllPieSlices(name: string) { + return await this.testSubjects.findAll(`pieSlice-${name.split(' ').join('-')}`); + } + + async getPieSliceStyle(name: string) { + this.log.debug(`VisualizePage.getPieSliceStyle(${name})`); + const pieSlice = await this.getPieSlice(name); + return await pieSlice.getAttribute('style'); + } + + async getAllPieSliceStyles(name: string) { + this.log.debug(`VisualizePage.getAllPieSliceStyles(${name})`); + const pieSlices = await this.getAllPieSlices(name); + return await Promise.all( + pieSlices.map(async (pieSlice) => await pieSlice.getAttribute('style')) + ); + } + + async getPieChartData() { + const chartTypes = await this.find.allByCssSelector('path.slice', this.defaultFindTimeout * 2); + return await Promise.all(chartTypes.map(async (chart) => await chart.getAttribute('d'))); + } + + async expectPieChartTableData(expectedTableData: Array<[]>) { + await this.inspector.open(); + await this.inspector.setTablePageSize(50); + await this.inspector.expectTableData(expectedTableData); + } + + async getPieChartLabels() { + const chartTypes = await this.find.allByCssSelector('path.slice', this.defaultFindTimeout * 2); + return await Promise.all( + chartTypes.map(async (chart) => await chart.getAttribute('data-label')) + ); + } + + async getPieSliceCount() { + this.log.debug('PieChart.getPieSliceCount'); + const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice'); + return slices.length; + } + + async expectPieSliceCount(expectedCount: number) { + this.log.debug(`PieChart.expectPieSliceCount(${expectedCount})`); + await this.retry.try(async () => { + const slicesCount = await this.getPieSliceCount(); + expect(slicesCount).to.be(expectedCount); + }); + } + + async expectPieChartLabels(expectedLabels: string[]) { + this.log.debug(`PieChart.expectPieChartLabels(${expectedLabels.join(',')})`); + await this.retry.try(async () => { + const pieData = await this.getPieChartLabels(); + expect(pieData).to.eql(expectedLabels); + }); + } } diff --git a/test/functional/services/visualizations/vega_debug_inspector.ts b/test/functional/services/visualizations/vega_debug_inspector.ts index af61a5aee4dea9..f85d1d51107eb3 100644 --- a/test/functional/services/visualizations/vega_debug_inspector.ts +++ b/test/functional/services/visualizations/vega_debug_inspector.ts @@ -6,53 +6,49 @@ * Side Public License, v 1. */ -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrService } from '../../ftr_provider_context'; -export function VegaDebugInspectorViewProvider({ getService }: FtrProviderContext) { - const testSubjects = getService('testSubjects'); - const inspector = getService('inspector'); - const dataGrid = getService('dataGrid'); +export class VegaDebugInspectorViewService extends FtrService { + private readonly testSubjects = this.ctx.getService('testSubjects'); + private readonly inspector = this.ctx.getService('inspector'); + private readonly dataGrid = this.ctx.getService('dataGrid'); - class VegaDebugInspectorView { - async openVegaDebugInspectorView() { - await inspector.openInspectorView('inspectorViewChooserVega debug'); - } - - public getOpenDataViewerButton() { - return testSubjects.find('vegaDataInspectorDataViewerButton'); - } + async openVegaDebugInspectorView() { + await this.inspector.openInspectorView('inspectorViewChooserVega debug'); + } - public getOpenSignalViewerButton() { - return testSubjects.find('vegaDataInspectorSignalViewerButton'); - } + public getOpenDataViewerButton() { + return this.testSubjects.find('vegaDataInspectorDataViewerButton'); + } - public getOpenSpecViewerButton() { - return testSubjects.find('vegaDataInspectorSpecViewerButton'); - } + public getOpenSignalViewerButton() { + return this.testSubjects.find('vegaDataInspectorSignalViewerButton'); + } - public getCopyClipboardButton() { - return testSubjects.find('vegaDataInspectorCopyClipboardButton'); - } + public getOpenSpecViewerButton() { + return this.testSubjects.find('vegaDataInspectorSpecViewerButton'); + } - public getGridTableData() { - return dataGrid.getDataGridTableData(); - } + public getCopyClipboardButton() { + return this.testSubjects.find('vegaDataInspectorCopyClipboardButton'); + } - public async navigateToDataViewerTab() { - const dataViewerButton = await this.getOpenDataViewerButton(); - await dataViewerButton.click(); - } + public getGridTableData() { + return this.dataGrid.getDataGridTableData(); + } - public async navigateToSignalViewerTab() { - const signalViewerButton = await this.getOpenSignalViewerButton(); - await signalViewerButton.click(); - } + public async navigateToDataViewerTab() { + const dataViewerButton = await this.getOpenDataViewerButton(); + await dataViewerButton.click(); + } - public async navigateToSpecViewerTab() { - const specViewerButton = await this.getOpenSpecViewerButton(); - await specViewerButton.click(); - } + public async navigateToSignalViewerTab() { + const signalViewerButton = await this.getOpenSignalViewerButton(); + await signalViewerButton.click(); } - return new VegaDebugInspectorView(); + public async navigateToSpecViewerTab() { + const specViewerButton = await this.getOpenSpecViewerButton(); + await specViewerButton.click(); + } } diff --git a/test/package/Vagrantfile b/test/package/Vagrantfile new file mode 100644 index 00000000000000..34c29eb2cefe71 --- /dev/null +++ b/test/package/Vagrantfile @@ -0,0 +1,27 @@ +Vagrant.configure("2") do |config| + config.vm.synced_folder '../../target/', '/packages' + + config.vm.define "deb" do |deb| + deb.vm.box = 'elastic/debian-9-x86_64' + deb.vm.provision "ansible" do |ansible| + ansible.playbook = "deb.yml" + end + deb.vm.network "private_network", ip: "192.168.50.5" + end + + config.vm.define "rpm" do |rpm| + rpm.vm.box = 'elastic/centos-7-x86_64' + rpm.vm.provision "ansible" do |ansible| + ansible.playbook = "rpm.yml" + end + rpm.vm.network "private_network", ip: "192.168.50.6" + end + + config.vm.define "docker" do |docker| + docker.vm.box = 'elastic/ubuntu-18.04-x86_64' + docker.vm.provision "ansible" do |ansible| + ansible.playbook = "docker.yml" + end + docker.vm.network "private_network", ip: "192.168.50.7" + end +end diff --git a/test/package/deb.yml b/test/package/deb.yml new file mode 100644 index 00000000000000..294657e99473f7 --- /dev/null +++ b/test/package/deb.yml @@ -0,0 +1,11 @@ +- name: test kibana deb package + hosts: deb + roles: + - install_kibana_deb + - assert_keystore_available + - assert_keystore_cli + - assert_kibana_yml + - assert_kibana_listening + - assert_kibana_available + - assert_kibana_log + - assert_kibana_data diff --git a/test/package/docker.yml b/test/package/docker.yml new file mode 100644 index 00000000000000..6da20360c174ad --- /dev/null +++ b/test/package/docker.yml @@ -0,0 +1,7 @@ +- name: test kibana docker package + hosts: docker + roles: + - install_docker + - install_kibana_docker + - assert_kibana_listening + - assert_kibana_available diff --git a/test/package/group_vars/all.yml b/test/package/group_vars/all.yml new file mode 100644 index 00000000000000..cedf02162271ab --- /dev/null +++ b/test/package/group_vars/all.yml @@ -0,0 +1,2 @@ +elasticsearch_username: kibana_system +elasticsearch_password: changeme diff --git a/test/package/roles/assert_keystore_available/tasks/main.yml b/test/package/roles/assert_keystore_available/tasks/main.yml new file mode 100644 index 00000000000000..cc0b3c187aa27e --- /dev/null +++ b/test/package/roles/assert_keystore_available/tasks/main.yml @@ -0,0 +1,13 @@ +- name: stat + become: yes + register: keystore + stat: + path: /etc/kibana/kibana.keystore + +- name: 0660 root:kibana + assert: + that: + - keystore.stat.exists + - keystore.stat.mode == "0660" + - keystore.stat.pw_name == "root" + - keystore.stat.gr_name == "kibana" diff --git a/test/package/roles/assert_keystore_cli/tasks/main.yml b/test/package/roles/assert_keystore_cli/tasks/main.yml new file mode 100644 index 00000000000000..4ca2622aa5c014 --- /dev/null +++ b/test/package/roles/assert_keystore_cli/tasks/main.yml @@ -0,0 +1,22 @@ +- name: "add server.name: package-testing" + become: yes + command: + cmd: /usr/share/kibana/bin/kibana-keystore add server.name --stdin + stdin: package-testing + register: kibana_keystore_add + +- debug: + msg: "{{ kibana_keystore_add.stdout }}" + +- name: register kibana-keystore list + become: yes + command: /usr/share/kibana/bin/kibana-keystore list + register: kibana_keystore_list + +- debug: + msg: "{{ kibana_keystore_list.stdout }}" + +- name: assert kibana-keystore list contains server.name + assert: + that: + - kibana_keystore_list.stdout == "server.name" diff --git a/test/package/roles/assert_kibana_available/tasks/main.yml b/test/package/roles/assert_kibana_available/tasks/main.yml new file mode 100644 index 00000000000000..b096f9b87350d2 --- /dev/null +++ b/test/package/roles/assert_kibana_available/tasks/main.yml @@ -0,0 +1,8 @@ +- name: "localhost:5601/api/status" + uri: + url: "http://localhost:5601/api/status" + status_code: [200, 401] + register: result + until: result.status != 503 + retries: 3 + delay: 30 diff --git a/test/package/roles/assert_kibana_data/tasks/main.yml b/test/package/roles/assert_kibana_data/tasks/main.yml new file mode 100644 index 00000000000000..76fba22e3697a9 --- /dev/null +++ b/test/package/roles/assert_kibana_data/tasks/main.yml @@ -0,0 +1,13 @@ +- name: stat /var/lib/kibana + become: yes + register: kibana_data_directory + stat: + path: /var/lib/kibana + +- name: /var/lib/kibana 2750 kibana:kibana + assert: + that: + - kibana_log_directory.stat.exists + - kibana_log_directory.stat.mode == "2750" + - kibana_log_directory.stat.pw_name == "kibana" + - kibana_log_directory.stat.gr_name == "kibana" diff --git a/test/package/roles/assert_kibana_listening/tasks/main.yml b/test/package/roles/assert_kibana_listening/tasks/main.yml new file mode 100644 index 00000000000000..3c072ce426404a --- /dev/null +++ b/test/package/roles/assert_kibana_listening/tasks/main.yml @@ -0,0 +1,2 @@ +- name: localhost:5601 + wait_for: host=localhost port=5601 timeout=60 diff --git a/test/package/roles/assert_kibana_log/tasks/main.yml b/test/package/roles/assert_kibana_log/tasks/main.yml new file mode 100644 index 00000000000000..d0c2d36caa0efd --- /dev/null +++ b/test/package/roles/assert_kibana_log/tasks/main.yml @@ -0,0 +1,27 @@ +- name: stat /var/log/kibana + become: yes + register: kibana_log_directory + stat: + path: /var/log/kibana + +- name: /var/log/kibana 2750 kibana:kibana + assert: + that: + - kibana_log_directory.stat.exists + - kibana_log_directory.stat.mode == "2750" + - kibana_log_directory.stat.pw_name == "kibana" + - kibana_log_directory.stat.gr_name == "kibana" + +- name: stat /var/log/kibana/kibana.log + become: yes + register: kibana_log + stat: + path: /var/log/kibana/kibana.log + +- name: /var/log/kibana/kibana.log 0644 kibana:kibana + assert: + that: + - kibana_log.stat.exists + - kibana_log.stat.mode == "0644" + - kibana_log.stat.pw_name == "kibana" + - kibana_log.stat.gr_name == "kibana" diff --git a/test/package/roles/assert_kibana_pid/tasks/main.yml b/test/package/roles/assert_kibana_pid/tasks/main.yml new file mode 100644 index 00000000000000..a77ab2a743d1fa --- /dev/null +++ b/test/package/roles/assert_kibana_pid/tasks/main.yml @@ -0,0 +1,27 @@ +- name: stat /run/kibana + become: yes + register: kibana_pid_directory + stat: + path: /run/kibana + +- name: /run/kibana 0775 kibana:kibana + assert: + that: + - kibana_pid_directory.stat.exists + - kibana_pid_directory.stat.mode == "0775" + - kibana_pid_directory.stat.pw_name == "kibana" + - kibana_pid_directory.stat.gr_name == "kibana" + +- name: stat /run/kibana/kibana.pid + become: yes + register: kibana_pid + stat: + path: /run/kibana/kibana.pid + +- name: /run/kibana/kibana.pid 0644 kibana:kibana + assert: + that: + - kibana_pid.stat.exists + - kibana_pid.stat.mode == "0644" + - kibana_pid.stat.pw_name == "kibana" + - kibana_pid.stat.gr_name == "kibana" diff --git a/test/package/roles/assert_kibana_yml/tasks/main.yml b/test/package/roles/assert_kibana_yml/tasks/main.yml new file mode 100644 index 00000000000000..02f6c1b7dd1e4b --- /dev/null +++ b/test/package/roles/assert_kibana_yml/tasks/main.yml @@ -0,0 +1,27 @@ +- name: stat /etc/kibana + become: yes + register: kibana_yml_directory + stat: + path: /etc/kibana + +- name: /etc/kibana 2750 root:kibana + assert: + that: + - kibana_yml_directory.stat.exists + - kibana_yml_directory.stat.mode == "2750" + - kibana_yml_directory.stat.pw_name == "root" + - kibana_yml_directory.stat.gr_name == "kibana" + +- name: stat /etc/kibana/kibana.yml + become: yes + register: kibana_yml + stat: + path: /etc/kibana/kibana.yml + +- name: /etc/kibana/kibana.yml 0660 root:kibana + assert: + that: + - kibana_yml.stat.exists + - kibana_yml.stat.mode == "0660" + - kibana_yml.stat.pw_name == "root" + - kibana_yml.stat.gr_name == "kibana" diff --git a/test/package/roles/install_docker/tasks/main.yml b/test/package/roles/install_docker/tasks/main.yml new file mode 100644 index 00000000000000..c985fdcb350ea7 --- /dev/null +++ b/test/package/roles/install_docker/tasks/main.yml @@ -0,0 +1,39 @@ +- name: install dependencies + become: yes + apt: + name: '{{ docker_dependencies }}' + state: present + update_cache: yes + +- name: add docker gpg key + become: yes + apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + +- name: fetch ubuntu version + shell: lsb_release -cs + register: ubuntu_version + changed_when: false + +- name: add docker repository + become: yes + apt_repository: + repo: 'deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ubuntu_version.stdout }} stable' + state: present + +- name: update apt packages + become: yes + apt: + update_cache: yes + +- name: install docker + become: yes + apt: + name: 'docker-ce' + state: present + +- name: install docker sdk + become: yes + pip: + name: docker diff --git a/test/package/roles/install_docker/vars/main.yml b/test/package/roles/install_docker/vars/main.yml new file mode 100644 index 00000000000000..ba561ba2e2d91f --- /dev/null +++ b/test/package/roles/install_docker/vars/main.yml @@ -0,0 +1,7 @@ +docker_dependencies: + - apt-transport-https + - ca-certificates + - curl + - gnupg-agent + - software-properties-common + - python3-pip diff --git a/test/package/roles/install_kibana_deb/tasks/main.yml b/test/package/roles/install_kibana_deb/tasks/main.yml new file mode 100644 index 00000000000000..570fd2d06f173d --- /dev/null +++ b/test/package/roles/install_kibana_deb/tasks/main.yml @@ -0,0 +1,34 @@ +- name: install dependencies + become: yes + apt: + name: + - libnss3 + - fonts-liberation + - libfontconfig + state: latest + +- name: find deb package + find: + paths: /packages/ + patterns: kibana-*-amd64.deb + register: kibana_deb + +- name: install + become: yes + apt: + deb: "{{ kibana_deb.files[0].path }}" + state: present + +- name: copy configuration + become: yes + template: + src: templates/kibana.yml + dest: /etc/kibana/kibana.yml + register: config + +- name: start kibana + become: yes + systemd: + state: started + name: kibana + daemon_reload: yes diff --git a/test/package/roles/install_kibana_docker/tasks/main.yml b/test/package/roles/install_kibana_docker/tasks/main.yml new file mode 100644 index 00000000000000..6d2f0a2caed1c1 --- /dev/null +++ b/test/package/roles/install_kibana_docker/tasks/main.yml @@ -0,0 +1,26 @@ +- name: find docker image + find: + paths: /packages/ + patterns: kibana-*-docker-image.tar.gz + register: kibana_docker + +- name: load image + become: yes + docker_image: + name: kibana + load_path: "{{ kibana_docker.files[0].path }}" + timeout: 300 + source: load + state: present + +- name: start kibana + become: yes + docker_container: + name: kibana + image: "{{ kibana_docker.files[0].path | basename| regex_replace('kibana-(.*)-docker-image.tar.gz', 'docker.elastic.co/kibana/kibana:\\1') }}" + network_mode: host + env: + SERVER_HOST: 0.0.0.0 + ELASTICSEARCH_HOSTS: http://192.168.50.1:9200 + ELASTICSEARCH_USERNAME: "{{ elasticsearch_username }}" + ELASTICSEARCH_PASSWORD: "{{ elasticsearch_password }}" diff --git a/test/package/roles/install_kibana_rpm/tasks/main.yml b/test/package/roles/install_kibana_rpm/tasks/main.yml new file mode 100644 index 00000000000000..d29dba33f19b80 --- /dev/null +++ b/test/package/roles/install_kibana_rpm/tasks/main.yml @@ -0,0 +1,45 @@ +- name: install dependencies + become: yes + yum: + name: + - nss + - fontconfig + - freetype + state: latest + +- name: find rpm package + find: + paths: /packages/ + patterns: kibana-*-x86_64.rpm + register: kibana_rpm + +- name: install + become: yes + yum: + name: "{{ kibana_rpm.files[0].path }}" + state: present + disable_gpg_check: yes + +- name: copy configuration + become: yes + template: + src: templates/kibana.yml + dest: /etc/kibana/kibana.yml + register: config + +- name: open port 5601/tcp + become: yes + command: + cmd: firewall-cmd --zone=public --permanent --add-port=5601/tcp + +- name: reload firewall + become: yes + command: + cmd: firewall-cmd --reload + +- name: start kibana + become: yes + systemd: + state: started + name: kibana + daemon_reload: yes diff --git a/test/package/roles/upgrade_apt_packages/tasks/main.yml b/test/package/roles/upgrade_apt_packages/tasks/main.yml new file mode 100644 index 00000000000000..7dd4000d88ad23 --- /dev/null +++ b/test/package/roles/upgrade_apt_packages/tasks/main.yml @@ -0,0 +1,6 @@ +- name: upgrade apt packages + become: yes + apt: + name: '*' + state: latest + update_cache: yes diff --git a/test/package/roles/upgrade_yum_packages/tasks/main.yml b/test/package/roles/upgrade_yum_packages/tasks/main.yml new file mode 100644 index 00000000000000..fa874772cedcc2 --- /dev/null +++ b/test/package/roles/upgrade_yum_packages/tasks/main.yml @@ -0,0 +1,6 @@ +- name: upgrade yum packages + become: yes + yum: + name: '*' + state: latest + update_cache: yes diff --git a/test/package/rpm.yml b/test/package/rpm.yml new file mode 100644 index 00000000000000..456c2bdf18b72f --- /dev/null +++ b/test/package/rpm.yml @@ -0,0 +1,11 @@ +- name: test kibana rpm package + hosts: rpm + roles: + - install_kibana_rpm + - assert_keystore_available + - assert_keystore_cli + - assert_kibana_yml + - assert_kibana_listening + - assert_kibana_available + - assert_kibana_log + - assert_kibana_data diff --git a/test/package/templates/kibana.yml b/test/package/templates/kibana.yml new file mode 100644 index 00000000000000..a5e44b7acb0189 --- /dev/null +++ b/test/package/templates/kibana.yml @@ -0,0 +1,5 @@ +server.host: 0.0.0.0 + +elasticsearch.hosts: http://192.168.50.1:9200 +elasticsearch.username: "{{ elasticsearch_username }}" +elasticsearch.password: "{{ elasticsearch_password }}" diff --git a/test/scripts/jenkins_xpack_package_build.sh b/test/scripts/jenkins_xpack_package_build.sh new file mode 100755 index 00000000000000..698129a2d253bd --- /dev/null +++ b/test/scripts/jenkins_xpack_package_build.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e + +source src/dev/ci_setup/setup_env.sh + +export TMP=/tmp +export TMPDIR=/tmp + +node scripts/build --all-platforms --debug --no-oss + +gsutil -q -m cp 'target/*' "gs://ci-artifacts.kibana.dev/package-testing/$GIT_COMMIT/" diff --git a/test/scripts/jenkins_xpack_package_deb.sh b/test/scripts/jenkins_xpack_package_deb.sh new file mode 100755 index 00000000000000..42098a6464f530 --- /dev/null +++ b/test/scripts/jenkins_xpack_package_deb.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -e + +source src/dev/ci_setup/setup_env.sh + +gsutil -q -m cp "gs://ci-artifacts.kibana.dev/package-testing/$GIT_COMMIT/kibana-*.deb" ./target + +export VAGRANT_CWD=test/package +vagrant up deb --no-provision + +node scripts/es snapshot \ + -E network.bind_host=127.0.0.1,192.168.50.1 \ + -E discovery.type=single-node \ + --license=trial & +while ! timeout 1 bash -c "echo > /dev/tcp/localhost/9200"; do sleep 30; done + +vagrant provision deb + +export TEST_BROWSER_HEADLESS=1 +export TEST_KIBANA_URL=http://elastic:changeme@192.168.50.5:5601 +export TEST_ES_URL=http://elastic:changeme@192.168.50.1:9200 + +cd x-pack +node scripts/functional_test_runner.js --include-tag=smoke diff --git a/test/scripts/jenkins_xpack_package_docker.sh b/test/scripts/jenkins_xpack_package_docker.sh new file mode 100755 index 00000000000000..6cae2258303805 --- /dev/null +++ b/test/scripts/jenkins_xpack_package_docker.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -e + +source src/dev/ci_setup/setup_env.sh + +gsutil -q -m cp "gs://ci-artifacts.kibana.dev/package-testing/$GIT_COMMIT/kibana-[0-9]*-docker-image.tar.gz" ./target + +export VAGRANT_CWD=test/package +vagrant up docker --no-provision + +node scripts/es snapshot \ + -E network.bind_host=127.0.0.1,192.168.50.1 \ + -E discovery.type=single-node \ + --license=trial & +while ! timeout 1 bash -c "echo > /dev/tcp/localhost/9200"; do sleep 30; done + +vagrant provision docker + +export TEST_BROWSER_HEADLESS=1 +export TEST_KIBANA_URL=http://elastic:changeme@192.168.50.7:5601 +export TEST_ES_URL=http://elastic:changeme@192.168.50.1:9200 + +cd x-pack +node scripts/functional_test_runner.js --include-tag=smoke diff --git a/test/scripts/jenkins_xpack_package_rpm.sh b/test/scripts/jenkins_xpack_package_rpm.sh new file mode 100755 index 00000000000000..6aa7754ee4b213 --- /dev/null +++ b/test/scripts/jenkins_xpack_package_rpm.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -e + +source src/dev/ci_setup/setup_env.sh + +gsutil -q -m cp "gs://ci-artifacts.kibana.dev/package-testing/$GIT_COMMIT/kibana-*.rpm" ./target + +export VAGRANT_CWD=test/package +vagrant up rpm --no-provision + +node scripts/es snapshot \ + -E network.bind_host=127.0.0.1,192.168.50.1 \ + -E discovery.type=single-node \ + --license=trial & +while ! timeout 1 bash -c "echo > /dev/tcp/localhost/9200"; do sleep 30; done + +vagrant provision rpm + +export TEST_BROWSER_HEADLESS=1 +export TEST_KIBANA_URL=http://elastic:changeme@192.168.50.6:5601 +export TEST_ES_URL=http://elastic:changeme@192.168.50.1:9200 + +cd x-pack +node scripts/functional_test_runner.js --include-tag=smoke diff --git a/tsconfig.json b/tsconfig.json index c56d4c6b8dc329..37fc9ee05a29b0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,8 +10,6 @@ "src/cli/**/*", "src/dev/**/*", "src/fixtures/**/*", - "src/legacy/**/*", - "src/optimize/**/*", "x-pack/tasks/**/*", ], @@ -75,7 +73,6 @@ { "path": "./x-pack/plugins/actions/tsconfig.json" }, { "path": "./x-pack/plugins/alerting/tsconfig.json" }, { "path": "./x-pack/plugins/apm/tsconfig.json" }, - { "path": "./x-pack/plugins/beats_management/tsconfig.json" }, { "path": "./x-pack/plugins/canvas/tsconfig.json" }, { "path": "./x-pack/plugins/cases/tsconfig.json" }, { "path": "./x-pack/plugins/cloud/tsconfig.json" }, diff --git a/tsconfig.refs.json b/tsconfig.refs.json index 7343165e4a4ad9..1b8a76d601e38c 100644 --- a/tsconfig.refs.json +++ b/tsconfig.refs.json @@ -58,7 +58,6 @@ { "path": "./x-pack/plugins/actions/tsconfig.json" }, { "path": "./x-pack/plugins/alerting/tsconfig.json" }, { "path": "./x-pack/plugins/apm/tsconfig.json" }, - { "path": "./x-pack/plugins/beats_management/tsconfig.json" }, { "path": "./x-pack/plugins/canvas/tsconfig.json" }, { "path": "./x-pack/plugins/cases/tsconfig.json" }, { "path": "./x-pack/plugins/cloud/tsconfig.json" }, diff --git a/vars/workers.groovy b/vars/workers.groovy index e0c5ddb358d09d..4f9fc789a04b35 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -103,6 +103,7 @@ def base(Map params, Closure closure) { "PR_TARGET_BRANCH=${env.ghprbTargetBranch ?: ''}", "PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}", "TEST_BROWSER_HEADLESS=1", + "GIT_COMMIT=${checkoutInfo.commit}", "GIT_BRANCH=${checkoutInfo.branch}", "TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it "BUILD_TS_REFS_DISABLE=true", // no need to build ts refs in bootstrap diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 2db2f31ae09c3d..b2c3a36ae34144 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -7,7 +7,6 @@ "xpack.eventLog": "plugins/event_log", "xpack.stackAlerts": "plugins/stack_alerts", "xpack.apm": "plugins/apm", - "xpack.beatsManagement": "plugins/beats_management", "xpack.canvas": "plugins/canvas", "xpack.cases": "plugins/cases", "xpack.cloud": "plugins/cloud", diff --git a/x-pack/examples/alerting_example/server/plugin.ts b/x-pack/examples/alerting_example/server/plugin.ts index f6131679874db2..2420be798ec84f 100644 --- a/x-pack/examples/alerting_example/server/plugin.ts +++ b/x-pack/examples/alerting_example/server/plugin.ts @@ -44,7 +44,12 @@ export class AlertingExamplePlugin implements Plugin { allowedHosts: ['*'], preconfiguredAlertHistoryEsIndex: false, preconfigured: {}, - proxyRejectUnauthorizedCertificates: true, - rejectUnauthorized: true, + proxyRejectUnauthorizedCertificates: true, // legacy + rejectUnauthorized: true, // legacy proxyBypassHosts: undefined, proxyOnlyHosts: undefined, maxResponseContentLength: new ByteSizeValue(1000000), @@ -429,6 +429,10 @@ describe('create()', () => { idleInterval: schema.duration().validate('1h'), pageSize: 100, }, + tls: { + verificationMode: 'full', + proxyVerificationMode: 'full', + }, }); const localActionTypeRegistryParams = { diff --git a/x-pack/plugins/actions/server/actions_config.mock.ts b/x-pack/plugins/actions/server/actions_config.mock.ts index fbd9a8cddbdcb9..19a43951377b67 100644 --- a/x-pack/plugins/actions/server/actions_config.mock.ts +++ b/x-pack/plugins/actions/server/actions_config.mock.ts @@ -15,7 +15,9 @@ const createActionsConfigMock = () => { ensureHostnameAllowed: jest.fn().mockReturnValue({}), ensureUriAllowed: jest.fn().mockReturnValue({}), ensureActionTypeEnabled: jest.fn().mockReturnValue({}), - isRejectUnauthorizedCertificatesEnabled: jest.fn().mockReturnValue(true), + getTLSSettings: jest.fn().mockReturnValue({ + verificationMode: 'full', + }), getProxySettings: jest.fn().mockReturnValue(undefined), getResponseSettings: jest.fn().mockReturnValue({ maxContentLength: 1000000, diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 925e77ca85fb26..93dad226e0c99b 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -27,8 +27,8 @@ const defaultActionsConfig: ActionsConfig = { enabledActionTypes: [], preconfiguredAlertHistoryEsIndex: false, preconfigured: {}, - proxyRejectUnauthorizedCertificates: true, - rejectUnauthorized: true, + proxyRejectUnauthorizedCertificates: true, // legacy + rejectUnauthorized: true, // legacy maxResponseContentLength: new ByteSizeValue(1000000), responseTimeout: moment.duration(60000), cleanupFailedExecutionsTask: { @@ -37,6 +37,10 @@ const defaultActionsConfig: ActionsConfig = { idleInterval: schema.duration().validate('1h'), pageSize: 100, }, + tls: { + proxyVerificationMode: 'full', + verificationMode: 'full', + }, }; describe('ensureUriAllowed', () => { @@ -305,22 +309,45 @@ describe('getProxySettings', () => { expect(proxySettings?.proxyUrl).toBe(config.proxyUrl); }); - test('returns proxyRejectUnauthorizedCertificates', () => { + test('returns proper verificationMode values, beased on the legacy config option proxyRejectUnauthorizedCertificates', () => { const configTrue: ActionsConfig = { ...defaultActionsConfig, proxyUrl: 'https://proxy.elastic.co', proxyRejectUnauthorizedCertificates: true, }; let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); - expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(true); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full'); const configFalse: ActionsConfig = { ...defaultActionsConfig, proxyUrl: 'https://proxy.elastic.co', proxyRejectUnauthorizedCertificates: false, + tls: {}, + }; + proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none'); + }); + + test('returns proper verificationMode value, based on the TLS proxy configuration', () => { + const configTrue: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + tls: { + proxyVerificationMode: 'full', + }, + }; + let proxySettings = getActionsConfigurationUtilities(configTrue).getProxySettings(); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('full'); + + const configFalse: ActionsConfig = { + ...defaultActionsConfig, + proxyUrl: 'https://proxy.elastic.co', + tls: { + proxyVerificationMode: 'none', + }, }; proxySettings = getActionsConfigurationUtilities(configFalse).getProxySettings(); - expect(proxySettings?.proxyRejectUnauthorizedCertificates).toBe(false); + expect(proxySettings?.proxyTLSSettings.verificationMode).toBe('none'); }); test('returns proxy headers', () => { @@ -406,13 +433,13 @@ describe('getProxySettings', () => { { url: 'https://elastic.co', tls: { - rejectUnauthorized: true, + verificationMode: 'full', }, }, { url: 'smtp://elastic.co:123', tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, smtp: { ignoreTLS: true, @@ -437,3 +464,25 @@ describe('getProxySettings', () => { expect(chs).toEqual(undefined); }); }); + +describe('getTLSSettings', () => { + test('returns proper verificationMode value, based on the TLS proxy configuration', () => { + const configTrue: ActionsConfig = { + ...defaultActionsConfig, + tls: { + verificationMode: 'full', + }, + }; + let tlsSettings = getActionsConfigurationUtilities(configTrue).getTLSSettings(); + expect(tlsSettings.verificationMode).toBe('full'); + + const configFalse: ActionsConfig = { + ...defaultActionsConfig, + tls: { + verificationMode: 'none', + }, + }; + tlsSettings = getActionsConfigurationUtilities(configFalse).getTLSSettings(); + expect(tlsSettings.verificationMode).toBe('none'); + }); +}); diff --git a/x-pack/plugins/actions/server/actions_config.ts b/x-pack/plugins/actions/server/actions_config.ts index b8cd5878a8972e..d25101f8279f88 100644 --- a/x-pack/plugins/actions/server/actions_config.ts +++ b/x-pack/plugins/actions/server/actions_config.ts @@ -14,7 +14,8 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { ActionsConfig, AllowedHosts, EnabledActionTypes, CustomHostSettings } from './config'; import { getCanonicalCustomHostUrl } from './lib/custom_host_settings'; import { ActionTypeDisabledError } from './lib'; -import { ProxySettings, ResponseSettings } from './types'; +import { ProxySettings, ResponseSettings, TLSSettings } from './types'; +import { getTLSSettingsFromConfig } from './builtin_action_types/lib/get_node_tls_options'; export { AllowedHosts, EnabledActionTypes } from './config'; @@ -30,7 +31,7 @@ export interface ActionsConfigurationUtilities { ensureHostnameAllowed: (hostname: string) => void; ensureUriAllowed: (uri: string) => void; ensureActionTypeEnabled: (actionType: string) => void; - isRejectUnauthorizedCertificatesEnabled: () => boolean; + getTLSSettings: () => TLSSettings; getProxySettings: () => undefined | ProxySettings; getResponseSettings: () => ResponseSettings; getCustomHostSettings: (targetUrl: string) => CustomHostSettings | undefined; @@ -93,7 +94,10 @@ function getProxySettingsFromConfig(config: ActionsConfig): undefined | ProxySet proxyBypassHosts: arrayAsSet(config.proxyBypassHosts), proxyOnlyHosts: arrayAsSet(config.proxyOnlyHosts), proxyHeaders: config.proxyHeaders, - proxyRejectUnauthorizedCertificates: config.proxyRejectUnauthorizedCertificates, + proxyTLSSettings: getTLSSettingsFromConfig( + config.tls?.proxyVerificationMode, + config.proxyRejectUnauthorizedCertificates + ), }; } @@ -142,8 +146,8 @@ export function getActionsConfigurationUtilities( isActionTypeEnabled, getProxySettings: () => getProxySettingsFromConfig(config), getResponseSettings: () => getResponseSettingsFromConfig(config), - // returns the global rejectUnauthorized setting - isRejectUnauthorizedCertificatesEnabled: () => config.rejectUnauthorized, + getTLSSettings: () => + getTLSSettingsFromConfig(config.tls?.verificationMode, config.rejectUnauthorized), ensureUriAllowed(uri: string) { if (!isUriAllowed(uri)) { throw new Error(allowListErrorMessage(AllowListingField.URL, uri)); diff --git a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts index 5747b4bbb28f4e..98ea436b17f3e1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/email.test.ts @@ -285,9 +285,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "content": Object { @@ -346,9 +346,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "content": Object { diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts index edc9429e4fac6a..ccd5a044971dfc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils.test.ts @@ -18,7 +18,7 @@ import { getCustomAgents } from './get_custom_agents'; const TestUrl = 'https://elastic.co/foo/bar/baz'; const logger = loggingSystemMock.create().get() as jest.Mocked; -const configurationUtilities = actionsConfigMock.create(); +let configurationUtilities = actionsConfigMock.create(); jest.mock('axios'); const axiosMock = (axios as unknown) as jest.Mock; @@ -42,6 +42,7 @@ describe('request', () => { headers: { 'content-type': 'application/json' }, data: { incidentId: '123' }, })); + configurationUtilities = actionsConfigMock.create(); configurationUtilities.getResponseSettings.mockReturnValue({ maxContentLength: 1000000, timeout: 360000, @@ -74,7 +75,9 @@ describe('request', () => { test('it have been called with proper proxy agent for a valid url', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://localhost:1212', proxyBypassHosts: undefined, proxyOnlyHosts: undefined, @@ -107,7 +110,9 @@ describe('request', () => { test('it have been called with proper proxy agent for an invalid url', async () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope:', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -136,7 +141,9 @@ describe('request', () => { test('it bypasses with proxyBypassHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: new Set(['elastic.co']), proxyOnlyHosts: undefined, @@ -157,7 +164,9 @@ describe('request', () => { test('it does not bypass with proxyBypassHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: new Set(['not-elastic.co']), proxyOnlyHosts: undefined, @@ -178,7 +187,9 @@ describe('request', () => { test('it proxies with proxyOnlyHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['elastic.co']), @@ -199,7 +210,9 @@ describe('request', () => { test('it does not proxy with proxyOnlyHosts when expected', async () => { configurationUtilities.getProxySettings.mockReturnValue({ - proxyRejectUnauthorizedCertificates: true, + proxyTLSSettings: { + verificationMode: 'full', + }, proxyUrl: 'https://elastic.proxy.co', proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-elastic.co']), @@ -252,6 +265,7 @@ describe('patch', () => { status: 200, headers: { 'content-type': 'application/json' }, })); + configurationUtilities = actionsConfigMock.create(); configurationUtilities.getResponseSettings.mockReturnValue({ maxContentLength: 1000000, timeout: 360000, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts index 80bf51e19c379c..235fca005e225f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts @@ -81,23 +81,25 @@ describe('axios connections', () => { await expect(fn()).rejects.toThrow('certificate'); }); - test('it works with rejectUnauthorized false config', async () => { + test('it works with verificationMode "none" config', async () => { const { url, server } = await createServer(true); testServer = server; const configurationUtilities = getACUfromConfig({ - rejectUnauthorized: false, + tls: { + verificationMode: 'none', + }, }); const res = await request({ axios, url, logger, configurationUtilities }); expect(res.status).toBe(200); }); - test('it works with rejectUnauthorized custom host config', async () => { + test('it works with verificationMode "none" for custom host config', async () => { const { url, server } = await createServer(true); testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url, tls: { rejectUnauthorized: false } }], + customHostSettings: [{ url, tls: { verificationMode: 'none' } }], }); const res = await request({ axios, url, logger, configurationUtilities }); expect(res.status).toBe(200); @@ -125,7 +127,7 @@ describe('axios connections', () => { await expect(fn()).rejects.toThrow('certificate'); }); - test('it works with incorrect ca in custom host config but rejectUnauthorized false', async () => { + test('it works with incorrect ca in custom host config but verificationMode "none"', async () => { const { url, server } = await createServer(true); testServer = server; @@ -135,7 +137,7 @@ describe('axios connections', () => { url, tls: { certificateAuthoritiesData: CA, - rejectUnauthorized: false, + verificationMode: 'none', }, }, ], @@ -144,12 +146,14 @@ describe('axios connections', () => { expect(res.status).toBe(200); }); - test('it works with incorrect ca in custom host config but rejectUnauthorized config true', async () => { + test('it works with incorrect ca in custom host config but verificationMode config "full"', async () => { const { url, server } = await createServer(true); testServer = server; const configurationUtilities = getACUfromConfig({ - rejectUnauthorized: false, + tls: { + verificationMode: 'none', + }, customHostSettings: [ { url, @@ -169,7 +173,7 @@ describe('axios connections', () => { testServer = server; const configurationUtilities = getACUfromConfig({ - customHostSettings: [{ url: otherUrl, tls: { rejectUnauthorized: false } }], + customHostSettings: [{ url: otherUrl, tls: { verificationMode: 'none' } }], }); const fn = async () => await request({ axios, url, logger, configurationUtilities }); await expect(fn()).rejects.toThrow('certificate'); @@ -251,6 +255,10 @@ const BaseActionsConfig: ActionsConfig = { proxyUrl: undefined, proxyHeaders: undefined, proxyRejectUnauthorizedCertificates: true, + tls: { + proxyVerificationMode: 'full', + verificationMode: 'full', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, rejectUnauthorized: true, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts index 805c22806ce4cf..8b4abe86e271ac 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.test.ts @@ -20,16 +20,19 @@ const targetUrlCanonical = `https://${targetHost}:443`; const nonMatchingUrl = `https://${targetHost}m/foo/bar/baz`; describe('getCustomAgents', () => { - const configurationUtilities = actionsConfigMock.create(); + let configurationUtilities = actionsConfigMock.create(); beforeEach(() => { jest.resetAllMocks(); + configurationUtilities = actionsConfigMock.create(); }); test('get agents for valid proxy URL', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -41,7 +44,9 @@ describe('getCustomAgents', () => { test('return default agents for invalid proxy URL', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: ':nope: not a valid URL', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -59,7 +64,9 @@ describe('getCustomAgents', () => { test('returns non-proxy agents for matching proxyBypassHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set([targetHost]), proxyOnlyHosts: undefined, }); @@ -71,7 +78,9 @@ describe('getCustomAgents', () => { test('returns proxy agents for non-matching proxyBypassHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set([targetHost]), proxyOnlyHosts: undefined, }); @@ -87,7 +96,9 @@ describe('getCustomAgents', () => { test('returns proxy agents for matching proxyOnlyHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set([targetHost]), }); @@ -99,7 +110,9 @@ describe('getCustomAgents', () => { test('returns non-proxy agents for non-matching proxyOnlyHosts', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set([targetHost]), }); @@ -116,7 +129,7 @@ describe('getCustomAgents', () => { configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', certificateAuthoritiesData: 'ca data here', }, }); @@ -128,14 +141,16 @@ describe('getCustomAgents', () => { test('handles custom host settings with proxy', () => { configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', certificateAuthoritiesData: 'ca data here', }, }); @@ -147,12 +162,14 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBe(false); }); - test('handles overriding global rejectUnauthorized false', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(false); + test('handles overriding global verificationMode "none"', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'none', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: true, + verificationMode: 'certificate', }, }); @@ -163,12 +180,14 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBeTruthy(); }); - test('handles overriding global rejectUnauthorized true', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(true); + test('handles overriding global verificationMode "full"', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'full', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, }); @@ -179,19 +198,23 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBeFalsy(); }); - test('handles overriding global rejectUnauthorized false with a proxy', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(false); + test('handles overriding global verificationMode "none" with a proxy', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'none', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: true, + verificationMode: 'full', }, }); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', // note: this setting doesn't come into play, it's for the connection to // the proxy, not the target url - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -202,19 +225,23 @@ describe('getCustomAgents', () => { expect(httpsAgent?.options.rejectUnauthorized).toBeTruthy(); }); - test('handles overriding global rejectUnauthorized true with a proxy', () => { - configurationUtilities.isRejectUnauthorizedCertificatesEnabled.mockReturnValue(true); + test('handles overriding global verificationMode "full" with a proxy', () => { + configurationUtilities.getTLSSettings.mockReturnValue({ + verificationMode: 'full', + }); configurationUtilities.getCustomHostSettings.mockReturnValue({ url: targetUrlCanonical, tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, }); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', // note: this setting doesn't come into play, it's for the connection to // the proxy, not the target url - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts index 6ec926004e73ed..a327ee3ffe931f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_custom_agents.ts @@ -11,6 +11,7 @@ import HttpProxyAgent from 'http-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent'; import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; +import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; interface GetCustomAgentsResponse { httpAgent: HttpAgent | undefined; @@ -22,12 +23,14 @@ export function getCustomAgents( logger: Logger, url: string ): GetCustomAgentsResponse { + const generalTLSSettings = configurationUtilities.getTLSSettings(); + const agentTLSOptions = getNodeTLSOptions(logger, generalTLSSettings.verificationMode); // the default for rejectUnauthorized is the global setting, which can // be overridden (below) with a custom host setting const defaultAgents = { httpAgent: undefined, httpsAgent: new HttpsAgent({ - rejectUnauthorized: configurationUtilities.isRejectUnauthorizedCertificatesEnabled(), + ...agentTLSOptions, }), }; @@ -50,10 +53,18 @@ export function getCustomAgents( agentOptions.ca = tlsSettings.certificateAuthoritiesData; } + const tlsSettingsFromConfig = getTLSSettingsFromConfig( + tlsSettings.verificationMode, + tlsSettings.rejectUnauthorized + ); // see: src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts // This is where the global rejectUnauthorized is overridden by a custom host - if (tlsSettings.rejectUnauthorized !== undefined) { - agentOptions.rejectUnauthorized = tlsSettings.rejectUnauthorized; + const customHostNodeTLSOptions = getNodeTLSOptions( + logger, + tlsSettingsFromConfig.verificationMode + ); + if (customHostNodeTLSOptions.rejectUnauthorized !== undefined) { + agentOptions.rejectUnauthorized = customHostNodeTLSOptions.rejectUnauthorized; } } @@ -96,6 +107,10 @@ export function getCustomAgents( return defaultAgents; } + const proxyNodeTLSOptions = getNodeTLSOptions( + logger, + proxySettings.proxyTLSSettings.verificationMode + ); // At this point, we are going to use a proxy, so we need new agents. // We will though, copy over the calculated tls options from above, into // the https agent. @@ -106,7 +121,7 @@ export function getCustomAgents( protocol: proxyUrl.protocol, headers: proxySettings.proxyHeaders, // do not fail on invalid certs if value is false - rejectUnauthorized: proxySettings.proxyRejectUnauthorizedCertificates, + ...proxyNodeTLSOptions, }) as unknown) as HttpsAgent; // vsCode wasn't convinced HttpsProxyAgent is an https.Agent, so we convinced it diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts new file mode 100644 index 00000000000000..7d131985053f17 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.test.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; + +const logger = loggingSystemMock.create().get() as jest.Mocked; + +describe('getNodeTLSOptions', () => { + test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "full"', () => { + const nodeOption = getNodeTLSOptions(logger, 'full'); + expect(nodeOption).toMatchObject({ + rejectUnauthorized: true, + }); + }); + + test('get node.js TLS options: rejectUnauthorized eql true for the verification mode "certificate"', () => { + const nodeOption = getNodeTLSOptions(logger, 'certificate'); + expect(nodeOption.checkServerIdentity).not.toBeNull(); + expect(nodeOption.rejectUnauthorized).toBeTruthy(); + }); + + test('get node.js TLS options: rejectUnauthorized eql false for the verification mode "none"', () => { + const nodeOption = getNodeTLSOptions(logger, 'none'); + expect(nodeOption).toMatchObject({ + rejectUnauthorized: false, + }); + }); + + test('get node.js TLS options: rejectUnauthorized eql true for the verification mode value which does not exist, the logger called with the proper warning message', () => { + const nodeOption = getNodeTLSOptions(logger, 'notexist'); + expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Unknown ssl verificationMode: notexist", + ], + ] + `); + expect(nodeOption).toMatchObject({ + rejectUnauthorized: true, + }); + }); +}); + +describe('getTLSSettingsFromConfig', () => { + test('get verificationMode eql "none" if legacy rejectUnauthorized eql false', () => { + const nodeOption = getTLSSettingsFromConfig(undefined, false); + expect(nodeOption).toMatchObject({ + verificationMode: 'none', + }); + }); + + test('get verificationMode eql "none" if legacy rejectUnauthorized eql true', () => { + const nodeOption = getTLSSettingsFromConfig(undefined, true); + expect(nodeOption).toMatchObject({ + verificationMode: 'full', + }); + }); + + test('get verificationMode eql "certificate", ignore rejectUnauthorized', () => { + const nodeOption = getTLSSettingsFromConfig('certificate', false); + expect(nodeOption).toMatchObject({ + verificationMode: 'certificate', + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts new file mode 100644 index 00000000000000..423e9756b13f8c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/get_node_tls_options.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PeerCertificate } from 'tls'; +import { TLSSettings } from '../../types'; +import { Logger } from '../../../../../../src/core/server'; + +export function getNodeTLSOptions( + logger: Logger, + verificationMode?: string +): { + rejectUnauthorized?: boolean; + checkServerIdentity?: ((host: string, cert: PeerCertificate) => Error | undefined) | undefined; +} { + const agentOptions: { + rejectUnauthorized?: boolean; + checkServerIdentity?: ((host: string, cert: PeerCertificate) => Error | undefined) | undefined; + } = {}; + if (!!verificationMode) { + switch (verificationMode) { + case 'none': + agentOptions.rejectUnauthorized = false; + break; + case 'certificate': + agentOptions.rejectUnauthorized = true; + // by default, NodeJS is checking the server identify + agentOptions.checkServerIdentity = () => undefined; + break; + case 'full': + agentOptions.rejectUnauthorized = true; + break; + default: { + logger.warn(`Unknown ssl verificationMode: ${verificationMode}`); + agentOptions.rejectUnauthorized = true; + } + } + // see: src/core/server/elasticsearch/legacy/elasticsearch_client_config.ts + // This is where the global rejectUnauthorized is overridden by a custom host + } + return agentOptions; +} + +export function getTLSSettingsFromConfig( + verificationMode?: 'none' | 'certificate' | 'full', + rejectUnauthorized?: boolean +): TLSSettings { + if (verificationMode) { + return { verificationMode }; + } else if (rejectUnauthorized !== undefined) { + return { verificationMode: rejectUnauthorized ? 'full' : 'none' }; + } + return {}; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts index cceeefde71dc2a..9bdb2d94811424 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.test.ts @@ -76,7 +76,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://example.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, } @@ -119,7 +121,7 @@ describe('send_email module', () => { `); }); - test('rejectUnauthorized default setting email using not secure host/port', async () => { + test('verificationMode default setting email using not secure host/port', async () => { const sendEmailOptions = getSendEmailOptions({ transport: { host: 'example.com', @@ -236,7 +238,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['example.com']), proxyOnlyHosts: undefined, } @@ -268,7 +272,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['not-example.com']), proxyOnlyHosts: undefined, } @@ -302,7 +308,9 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['example.com']), } @@ -336,7 +344,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: {}, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-example.com']), } @@ -453,7 +461,7 @@ describe('send_email module', () => { }, { proxyUrl: 'https://proxy.com', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: {}, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }, diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts index 005e73b1fc2f7d..9f601840bc9824 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/send_email.ts @@ -12,6 +12,7 @@ import { default as MarkdownIt } from 'markdown-it'; import { Logger } from '../../../../../../src/core/server'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { CustomHostSettings } from '../../config'; +import { getNodeTLSOptions, getTLSSettingsFromConfig } from './get_node_tls_options'; // an email "service" which doesn't actually send, just returns what it would send export const JSON_TRANSPORT_SERVICE = '__json'; @@ -58,7 +59,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom // eslint-disable-next-line @typescript-eslint/no-explicit-any const transportConfig: Record = {}; const proxySettings = configurationUtilities.getProxySettings(); - const rejectUnauthorized = configurationUtilities.isRejectUnauthorizedCertificatesEnabled(); + const generalTLSSettings = configurationUtilities.getTLSSettings(); if (hasAuth && user != null && password != null) { transportConfig.auth = { @@ -91,10 +92,10 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom customHostSettings = configurationUtilities.getCustomHostSettings(`smtp://${host}:${port}`); if (proxySettings && useProxy) { - transportConfig.tls = { - // do not fail on invalid certs if value is false - rejectUnauthorized: proxySettings?.proxyRejectUnauthorizedCertificates, - }; + transportConfig.tls = getNodeTLSOptions( + logger, + proxySettings?.proxyTLSSettings.verificationMode + ); transportConfig.proxy = proxySettings.proxyUrl; transportConfig.headers = proxySettings.proxyHeaders; } else if (!transportConfig.secure && user == null && password == null) { @@ -103,7 +104,7 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom // authenticate rarely have valid certs; eg cloud proxy, and npm maildev transportConfig.tls = { rejectUnauthorized: false }; } else { - transportConfig.tls = { rejectUnauthorized }; + transportConfig.tls = getNodeTLSOptions(logger, generalTLSSettings.verificationMode); } // finally, allow customHostSettings to override some of the settings @@ -116,14 +117,16 @@ export async function sendEmail(logger: Logger, options: SendEmailOptions): Prom if (tlsSettings?.certificateAuthoritiesData) { tlsConfig.ca = tlsSettings?.certificateAuthoritiesData; } - if (tlsSettings?.rejectUnauthorized !== undefined) { - tlsConfig.rejectUnauthorized = tlsSettings?.rejectUnauthorized; - } + const tlsSettingsFromConfig = getTLSSettingsFromConfig( + tlsSettings?.verificationMode, + tlsSettings?.rejectUnauthorized + ); + const nodeTLSOptions = getNodeTLSOptions(logger, tlsSettingsFromConfig.verificationMode); if (!transportConfig.tls) { - transportConfig.tls = tlsConfig; + transportConfig.tls = { ...tlsConfig, ...nodeTLSOptions }; } else { - transportConfig.tls = { ...transportConfig.tls, ...tlsConfig }; + transportConfig.tls = { ...transportConfig.tls, ...tlsConfig, ...nodeTLSOptions }; } if (smtpSettings?.ignoreTLS) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts index 76612696e8e583..4108424e26ac40 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/slack.test.ts @@ -194,7 +194,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: undefined, }); @@ -219,7 +221,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['example.com']), proxyOnlyHosts: undefined, }); @@ -244,7 +248,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: new Set(['not-example.com']), proxyOnlyHosts: undefined, }); @@ -269,7 +275,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['example.com']), }); @@ -294,7 +302,9 @@ describe('execute()', () => { const configurationUtilities = actionsConfigMock.create(); configurationUtilities.getProxySettings.mockReturnValue({ proxyUrl: 'https://someproxyhost', - proxyRejectUnauthorizedCertificates: false, + proxyTLSSettings: { + verificationMode: 'none', + }, proxyBypassHosts: undefined, proxyOnlyHosts: new Set(['not-example.com']), }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts index 95088fa5f79656..bf34789e03fae1 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/teams.test.ts @@ -170,9 +170,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": Object { @@ -234,9 +234,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": Object { diff --git a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts index 00e56303dbe220..b2c865c2f5374c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/webhook.test.ts @@ -293,9 +293,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": "some data", @@ -386,9 +386,9 @@ describe('execute()', () => { "getCustomHostSettings": [MockFunction], "getProxySettings": [MockFunction], "getResponseSettings": [MockFunction], + "getTLSSettings": [MockFunction], "isActionTypeEnabled": [MockFunction], "isHostnameAllowed": [MockFunction], - "isRejectUnauthorizedCertificatesEnabled": [MockFunction], "isUriAllowed": [MockFunction], }, "data": "some data", diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index 4c4fd143369e11..9774bfb05d4ff4 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -177,6 +177,44 @@ describe('config validation', () => { `"[customHostSettings.0.url]: expected value of type [string] but got [undefined]"` ); }); + + test('action with tls configuration', () => { + const config: Record = { + tls: { + verificationMode: 'none', + proxyVerificationMode: 'none', + }, + }; + expect(configSchema.validate(config)).toMatchInlineSnapshot(` + Object { + "allowedHosts": Array [ + "*", + ], + "cleanupFailedExecutionsTask": Object { + "cleanupInterval": "PT5M", + "enabled": true, + "idleInterval": "PT1H", + "pageSize": 100, + }, + "enabled": true, + "enabledActionTypes": Array [ + "*", + ], + "maxResponseContentLength": ByteSizeValue { + "valueInBytes": 1048576, + }, + "preconfigured": Object {}, + "preconfiguredAlertHistoryEsIndex": false, + "proxyRejectUnauthorizedCertificates": true, + "rejectUnauthorized": true, + "responseTimeout": "PT1M", + "tls": Object { + "proxyVerificationMode": "none", + "verificationMode": "none", + }, + } + `); + }); }); // object creator that ensures we can create a property named __proto__ on an diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 0dc1aed68f4d0c..8859a2d8881a25 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -33,7 +33,16 @@ const customHostSettingsSchema = schema.object({ ), tls: schema.maybe( schema.object({ + /** + * @deprecated in favor of `verificationMode` + **/ rejectUnauthorized: schema.maybe(schema.boolean()), + verificationMode: schema.maybe( + schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ) + ), certificateAuthoritiesFiles: schema.maybe( schema.oneOf([ schema.string({ minLength: 1 }), @@ -68,10 +77,32 @@ export const configSchema = schema.object({ }), proxyUrl: schema.maybe(schema.string()), proxyHeaders: schema.maybe(schema.recordOf(schema.string(), schema.string())), + /** + * @deprecated in favor of `tls.proxyVerificationMode` + **/ proxyRejectUnauthorizedCertificates: schema.boolean({ defaultValue: true }), proxyBypassHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), proxyOnlyHosts: schema.maybe(schema.arrayOf(schema.string({ hostname: true }))), + /** + * @deprecated in favor of `tls.verificationMode` + **/ rejectUnauthorized: schema.boolean({ defaultValue: true }), + tls: schema.maybe( + schema.object({ + verificationMode: schema.maybe( + schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ) + ), + proxyVerificationMode: schema.maybe( + schema.oneOf( + [schema.literal('none'), schema.literal('certificate'), schema.literal('full')], + { defaultValue: 'full' } + ) + ), + }) + ), maxResponseContentLength: schema.byteSize({ defaultValue: '1mb' }), responseTimeout: schema.duration({ defaultValue: '60s' }), customHostSettings: schema.maybe(schema.arrayOf(customHostSettingsSchema)), diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 99c6326d60e26d..6a0f06b34d670e 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -8,7 +8,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server'; import { ActionsPlugin } from './plugin'; -import { configSchema, ActionsConfig } from './config'; +import { configSchema, ActionsConfig, CustomHostSettings } from './config'; import { ActionsClient as ActionsClientClass } from './actions_client'; import { ActionsAuthorization as ActionsAuthorizationClass } from './authorization/actions_authorization'; @@ -57,7 +57,37 @@ export const plugin = (initContext: PluginInitializerContext) => new ActionsPlug export const config: PluginConfigDescriptor = { schema: configSchema, - deprecations: ({ renameFromRoot }) => [ + deprecations: ({ renameFromRoot, unused }) => [ renameFromRoot('xpack.actions.whitelistedHosts', 'xpack.actions.allowedHosts'), + (settings, fromPath, addDeprecation) => { + const customHostSettings = settings?.xpack?.actions?.customHostSettings ?? []; + if ( + customHostSettings.find( + (customHostSchema: CustomHostSettings) => + !!customHostSchema.tls && !!customHostSchema.tls.rejectUnauthorized + ) + ) { + addDeprecation({ + message: + '`xpack.actions.customHostSettings[].tls.rejectUnauthorized` is deprecated. Use `xpack.actions.customHostSettings[].tls.verificationMode` instead, with the setting `verificationMode:full` eql to `rejectUnauthorized:true`, and `verificationMode:none` eql to `rejectUnauthorized:false`.', + }); + } + }, + (settings, fromPath, addDeprecation) => { + if (!!settings?.xpack?.actions?.rejectUnauthorized) { + addDeprecation({ + message: + '`xpack.actions.rejectUnauthorized` is deprecated. Use `xpack.actions.verificationMode` instead, with the setting `verificationMode:full` eql to `rejectUnauthorized:true`, and `verificationMode:none` eql to `rejectUnauthorized:false`.', + }); + } + }, + (settings, fromPath, addDeprecation) => { + if (!!settings?.xpack?.actions?.proxyRejectUnauthorizedCertificates) { + addDeprecation({ + message: + '`xpack.actions.proxyRejectUnauthorizedCertificates` is deprecated. Use `xpack.actions.proxyVerificationMode` instead, with the setting `proxyVerificationMode:full` eql to `rejectUnauthorized:true`, and `proxyVerificationMode:none` eql to `rejectUnauthorized:false`.', + }); + } + }, ], }; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index ea22e90dfed405..c8c9967afca1a7 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -142,10 +142,14 @@ export interface ProxySettings { proxyBypassHosts: Set | undefined; proxyOnlyHosts: Set | undefined; proxyHeaders?: Record; - proxyRejectUnauthorizedCertificates: boolean; + proxyTLSSettings: TLSSettings; } export interface ResponseSettings { maxContentLength: number; timeout: number; } + +export interface TLSSettings { + verificationMode?: 'none' | 'certificate' | 'full'; +} diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 0d5b0bf415fed9..cb43e534080905 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -326,9 +326,10 @@ server.newPlatform.setup.plugins.alerting.registerType(myRuleType); ## Role Based Access-Control Once you have registered your AlertType, you need to grant your users privileges to use it. -When registering a feature in Kibana, you can specify multiple types of privileges which are granted to users when they're assigned certain roles. -Assuming your feature introduces its own AlertTypes, you'll want to control which roles have all/read privileges for these AlertTypes when they're inside the feature. -In addition, when users are inside your feature, you might want to grant them access to AlertTypes from other features, such as built-in stack rules or rule types provided by other features. +When registering a feature in Kibana you can specify multiple types of privileges which are granted to users when they're assigned certain roles. + +Assuming your feature introduces its own AlertTypes, you'll want to control which roles have all/read privileges for the rules and alerts for these AlertTypes when they're inside the feature. +In addition, when users are inside your feature, you might want to grant them access to rules and alerts for AlertTypes from other features, such as built-in stack rules or rule types provided by other features. You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example: @@ -337,30 +338,61 @@ features.registerKibanaFeature({ id: 'my-application-id', name: 'My Application', app: [], + alerting: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-restricted-rule-type', + '.index-threshold', + 'xpack.uptime.alerts.actionGroups.tls' + ], privileges: { all: { alerting: { - all: [ - // grant `all` over our own types - 'my-application-id.my-rule-type', - 'my-application-id.my-restricted-rule-type', - // grant `all` over the built-in IndexThreshold - '.index-threshold', - // grant `all` over Uptime's TLS rule type - 'xpack.uptime.alerts.actionGroups.tls' - ], + rule: { + all: [ + // grant `all` over our own types + 'my-application-id.my-rule-type', + 'my-application-id.my-restricted-rule-type', + // grant `all` over the built-in IndexThreshold + '.index-threshold', + // grant `all` over Uptime's TLS rule type + 'xpack.uptime.alerts.actionGroups.tls' + ], + }, + alert: { + all: [ + // grant `all` over our own types + 'my-application-id.my-rule-type', + 'my-application-id.my-restricted-rule-type', + // grant `all` over the built-in IndexThreshold + '.index-threshold', + // grant `all` over Uptime's TLS rule type + 'xpack.uptime.alerts.actionGroups.tls' + ], + } }, }, read: { alerting: { - read: [ - // grant `read` over our own type - 'my-application-id.my-rule-type', - // grant `read` over the built-in IndexThreshold - '.index-threshold', - // grant `read` over Uptime's TLS rule type - 'xpack.uptime.alerts.actionGroups.tls' - ], + rule: { + read: [ + // grant `read` over our own type + 'my-application-id.my-alert-type', + // grant `read` over the built-in IndexThreshold + '.index-threshold', + // grant `read` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls' + ], + }, + alert: { + read: [ + // grant `read` over our own type + 'my-application-id.my-alert-type', + // grant `read` over the built-in IndexThreshold + '.index-threshold', + // grant `read` over Uptime's TLS AlertType + 'xpack.uptime.alerts.actionGroups.tls' + ], + }, }, }, }, @@ -369,17 +401,21 @@ features.registerKibanaFeature({ In this example we can see the following: -- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting framework over every rule of the `my-application-id.my-rule-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these rules as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which grants the Alerting Framework's `read` role privileges over these very same rules. -- In addition, our feature grants the same privileges over any rule of type `my-application-id.my-restricted-rule-type`, which is another hypothetical rule type registered by this feature. It's worth noting that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with rules of this type. -- Next, lets look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over rules of these types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in stack rule type, provided by the _Stack Rules_ feature, and `xpack.uptime.alerts.actionGroups.tls` is a rule type provided by the _Uptime_ feature. Specifying these types here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's rule type is only possible if both the producer of the rule type and the consumer of the rule type explicitly grant privileges to do so. In this case, the _Stack Rules_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user. +- Our feature grants any user who's assigned the `all` role in our feature the `all` role in the Alerting Framework over every rule and alert of the rule type `my-application-id.my-rule-type` type which is created _inside_ the feature. What that means is that this privilege will allow the user to execute any of the `all` operations (listed below) on these rules and alerts as long as their `consumer` is `my-application-id`. Below that you'll notice we've done the same with the `read` role, which is grants the Alerting Framework's `read` role privileges over these very same rules and alerts. +- In addition, our feature grants the same privileges over any rule or alert of rule type `my-application-id.my-restricted-rule-type`, which is another hypothetical rule type registered by this feature. It's worth noting that this type has been omitted from the `read` role. What this means is that only users with the `all` role will be able to interact with rules and alerts of this rule type. +- Next, let's look at the `.index-threshold` and `xpack.uptime.alerts.actionGroups.tls` types. These have been specified in both `read` and `all`, which means that all the users in the feature will gain privileges over rules and alerts of these rule types (as long as their `consumer` is `my-application-id`). The difference between these two and the previous two is that they are _produced_ by other features! `.index-threshold` is a built-in stack rule type, provided by the _Stack Rules_ feature, and `xpack.uptime.alerts.actionGroups.tls` is a rule type provided by the _Uptime_ feature. Specifying these types here tells the Alerting Framework that as far as the `my-application-id` feature is concerned, the user is privileged to use them (with `all` and `read` applied), but that isn't enough. Using another feature's rule type is only possible if both the producer of the rule type and the consumer of the rule type explicitly grant privileges to do so. In this case, the _Stack Rules_ & _Uptime_ features would have to explicitly add these privileges to a role and this role would have to be granted to this user. -It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple types, for example: +It's important to note that any role can be granted a mix of `all` and `read` privileges across multiple types, for example: ```typescript features.registerKibanaFeature({ id: 'my-application-id', name: 'My Application', app: [], + alerting: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-restricted-rule-type' + ], privileges: { all: { app: ['my-application-id', 'kibana'], @@ -393,12 +429,22 @@ features.registerKibanaFeature({ read: { app: ['lens', 'kibana'], alerting: { - all: [ - 'my-application-id.my-rule-type' - ], - read: [ - 'my-application-id.my-restricted-rule-type' - ], + rule: { + all: [ + 'my-application-id.my-rule-type' + ], + read: [ + 'my-application-id.my-restricted-rule-type' + ], + }, + alert: { + all: [ + 'my-application-id.my-rule-type' + ], + read: [ + 'my-application-id.my-restricted-rule-type' + ], + }, }, savedObject: { all: [], @@ -414,6 +460,111 @@ features.registerKibanaFeature({ In the above example, note that instead of denying users with the `read` role any access to the `my-application-id.my-restricted-rule-type` type, we've decided that these users _should_ be granted `read` privileges over the _restricted_ rule type. As part of that same change, we also decided that not only should they be allowed to `read` the _restricted_ rule type, but actually, despite having `read` privileges to the feature as a whole, we do actually want to allow them to create our basic 'my-application-id.my-rule-type' rule type, as we consider it an extension of _reading_ data in our feature, rather than _writing_ it. +### Subfeature privileges + +In the above examples, we have been giving the same level of access to both rules and alerts for a particular rule type. There may be cases when you want your feature privilege to allow for escalated or de-escalated privileges for either rules or alerts within a feature. We can use subfeature privileges to achieve this granularity. + +For more information and other examples of subfeature privilege, refer to the [user documentation](https://www.elastic.co/guide/en/kibana/master/development-security.html#example-3-discover). + +```typescript +features.registerKibanaFeature({ + id: 'my-application-id', + name: 'My Application', + app: [], + alerting: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-other-rule-type' + ], + privileges: { + all: { + app: ['my-application-id', 'kibana'], + savedObject: { + all: [], + read: [], + }, + alerting: { + rule: { + all: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-other-rule-type' + ] + }, + alert: { + read: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-other-rule-type' + ] + } + }, + ui: [], + api: [], + }, + read: { + app: ['lens', 'kibana'], + alerting: { + rule: { + read: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-other-rule-type' + ] + }, + alert: { + read: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-other-rule-type' + ] + } + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + api: [], + }, + }, + subFeatures: [ + { + name: 'Manage Alerts', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'alert_manage', + name: 'Manage Alerts', + includeIn: 'all', + alerting: { + alert: { + all: [ + 'my-application-id.my-rule-type', + 'my-application-id.my-other-rule-type' + ], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + } + ] + } + ] + } + ] +}); +``` + +In the above example, note that the base feature privilege grants users with the `all` role `all` access the rules of the specified rule types but only `read` access to the alerts of the same rule type. In order to get `all` access to the alerts of these rule types, the role must grant the `alert_manage` subfeature privilege. Because the `alert_manage` subfeature privilege has `includeIn` set to `all`, it is _automatically_ included in the `all` feature privilege but can be excluded when the role is defined. + +This subfeature privilege definition allows for the following granularity: + +- `all` privileges to rules for a rule type and `all` privileges to alerts for a rule type +- `all` privileges to rules for a rule type and subprivilege de-escalation to grant only `read` privileges to alerts for a rule type +- `read` privileges to rules for a rule type and `read` privileges to alerts for a rule type +- `read` privileges to rules for a rule type and subprivilege escalation to grant `all` privileges to alerts for a rule type. + ### `read` privileges vs. `all` privileges When a user is granted the `read` role in the Alerting Framework, they will be able to execute the following api calls: diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 1b5e712a3ee696..2227e0cecd0a68 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -71,7 +71,12 @@ function mockFeature(appName: string, typeName?: string) { ...(typeName ? { alerting: { - all: [typeName], + rule: { + all: [typeName], + }, + alert: { + all: [typeName], + }, }, } : {}), @@ -85,7 +90,12 @@ function mockFeature(appName: string, typeName?: string) { ...(typeName ? { alerting: { - read: [typeName], + rule: { + read: [typeName], + }, + alert: { + read: [typeName], + }, }, } : {}), @@ -138,7 +148,9 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) { name: 'sub feature alert', includeIn: 'all', alerting: { - all: [typeName], + rule: { + all: [typeName], + }, }, savedObject: { all: [], @@ -151,7 +163,9 @@ function mockFeatureWithSubFeature(appName: string, typeName: string) { name: 'sub feature alert', includeIn: 'read', alerting: { - read: [typeName], + rule: { + read: [typeName], + }, }, savedObject: { all: [], @@ -269,7 +283,7 @@ describe('AlertingAuthorization', () => { expect(alertTypeRegistry.get).toHaveBeenCalledTimes(0); }); - test('ensures the user has privileges to execute the specified rule type, operation and alerting type without consumer when producer and consumer are the same', async () => { + test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when producer and consumer are the same', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -325,7 +339,63 @@ describe('AlertingAuthorization', () => { `); }); - test('ensures the user has privileges to execute the specified rule type, operation and alerting type without consumer when consumer is exempt', async () => { + test('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when producer and consumer are the same', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { kibana: [] }, + }); + + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myApp', + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'alert', + 'update' + ); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthorizationAction('myType', 'myApp', 'alert', 'update')], + }); + + expect(auditLogger.logAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myApp", + "update", + "alert", + ] + `); + }); + + test('ensures the user has privileges to execute rules for the specified rule type and operation without consumer when consumer is exempt', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -387,7 +457,69 @@ describe('AlertingAuthorization', () => { `); }); - test('ensures the user has privileges to execute the specified rule type, operation, alerting type and producer when producer is different from consumer', async () => { + test('ensures the user has privileges to execute alerts for the specified rule type and operation without consumer when consumer is exempt', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds: ['exemptConsumer'], + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { kibana: [] }, + }); + + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'exemptConsumer', + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'exemptConsumer', + 'alert', + 'update' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'alert', + 'update' + ); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthorizationAction('myType', 'myApp', 'alert', 'update')], + }); + + expect(auditLogger.logAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "exemptConsumer", + "update", + "alert", + ] + `); + }); + + test('ensures the user has privileges to execute rules for the specified rule type, operation and producer when producer is different from consumer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -452,7 +584,72 @@ describe('AlertingAuthorization', () => { `); }); - test('throws if user lacks the required privileges for the consumer', async () => { + test('ensures the user has privileges to execute alerts for the specified rule type, operation and producer when producer is different from consumer', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: true, + privileges: { kibana: [] }, + }); + + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds, + }); + + await alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myOtherApp', + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }); + + expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); + + expect(authorization.actions.alerting.get).toHaveBeenCalledTimes(2); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myApp', + 'alert', + 'update' + ); + expect(authorization.actions.alerting.get).toHaveBeenCalledWith( + 'myType', + 'myOtherApp', + 'alert', + 'update' + ); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [ + mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update'), + mockAuthorizationAction('myType', 'myApp', 'alert', 'update'), + ], + }); + + expect(auditLogger.logAuthorizationSuccess).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myOtherApp", + "update", + "alert", + ] + `); + }); + + test('throws if user lacks the required rule privileges for the consumer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -510,7 +707,7 @@ describe('AlertingAuthorization', () => { `); }); - test('throws if user lacks the required privieleges for the producer', async () => { + test('throws if user lacks the required alert privileges for the consumer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -532,11 +729,73 @@ describe('AlertingAuthorization', () => { privileges: { kibana: [ { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'create'), + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'update'), authorized: true, }, { - privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'create'), + privilege: mockAuthorizationAction('myType', 'myAppRulesOnly', 'alert', 'update'), + authorized: false, + }, + ], + }, + }); + + await expect( + alertAuthorization.ensureAuthorized({ + ruleTypeId: 'myType', + consumer: 'myAppRulesOnly', + operation: WriteOperations.Update, + entity: AlertingAuthorizationEntity.Alert, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Unauthorized to update a \\"myType\\" alert for \\"myAppRulesOnly\\""` + ); + + expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); + expect(auditLogger.logAuthorizationFailure).toHaveBeenCalledTimes(1); + expect(auditLogger.logAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "some-user", + "myType", + 0, + "myAppRulesOnly", + "update", + "alert", + ] + `); + }); + + test('throws if user lacks the required privileges for the producer', async () => { + const { authorization } = mockSecurity(); + const checkPrivileges: jest.MockedFunction< + ReturnType + > = jest.fn(); + authorization.checkPrivilegesDynamicallyWithRequest.mockReturnValue(checkPrivileges); + const alertAuthorization = new AlertingAuthorization({ + request, + authorization, + alertTypeRegistry, + features, + auditLogger, + getSpace, + exemptConsumerIds, + }); + + checkPrivileges.mockResolvedValueOnce({ + username: 'some-user', + hasAllRequested: false, + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'alert', 'update'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'alert', 'update'), authorized: false, }, ], @@ -547,11 +806,11 @@ describe('AlertingAuthorization', () => { alertAuthorization.ensureAuthorized({ ruleTypeId: 'myType', consumer: 'myOtherApp', - operation: WriteOperations.Create, + operation: WriteOperations.Update, entity: AlertingAuthorizationEntity.Alert, }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Unauthorized to create a \\"myType\\" alert by \\"myApp\\""` + `"Unauthorized to update a \\"myType\\" alert by \\"myApp\\""` ); expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); @@ -562,13 +821,13 @@ describe('AlertingAuthorization', () => { "myType", 1, "myApp", - "create", + "update", "alert", ] `); }); - test('throws if user lacks the required privieleges for both consumer and producer', async () => { + test('throws if user lacks the required privileges for both consumer and producer', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -662,7 +921,6 @@ describe('AlertingAuthorization', () => { enabledInLicense: true, }; const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]); - test('omits filter when there is no authorization api', async () => { const alertAuthorization = new AlertingAuthorization({ request, @@ -672,7 +930,6 @@ describe('AlertingAuthorization', () => { getSpace, exemptConsumerIds, }); - const { filter, ensureRuleTypeIsAuthorized, @@ -683,13 +940,10 @@ describe('AlertingAuthorization', () => { consumer: 'consumer', }, }); - expect(() => ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule')).not.toThrow(); - expect(filter).toEqual(undefined); }); - - test('ensureAlertTypeIsAuthorized is no-op when there is no authorization api', async () => { + test('ensureRuleTypeIsAuthorized is no-op when there is no authorization api', async () => { const alertAuthorization = new AlertingAuthorization({ request, alertTypeRegistry, @@ -698,7 +952,6 @@ describe('AlertingAuthorization', () => { getSpace, exemptConsumerIds, }); - const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Rule, { @@ -709,13 +962,10 @@ describe('AlertingAuthorization', () => { }, } ); - ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule'); - expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); }); - test('creates a filter based on the privileged types', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< @@ -727,7 +977,6 @@ describe('AlertingAuthorization', () => { hasAllRequested: true, privileges: { kibana: [] }, }); - const alertAuthorization = new AlertingAuthorization({ request, authorization, @@ -738,7 +987,6 @@ describe('AlertingAuthorization', () => { exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - expect( ( await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { @@ -754,11 +1002,9 @@ describe('AlertingAuthorization', () => { `((path.to.rule.id:myAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:myOtherAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule.id:mySecondAppAlertType and consumer-field:(myApp or myOtherApp or myAppWithSubFeature)))` ) ); - expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); }); - - test('creates an `ensureAlertTypeIsAuthorized` function which throws if type is unauthorized', async () => { + test('creates an `ensureRuleTypeIsAuthorized` function which throws if type is unauthorized', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -793,7 +1039,6 @@ describe('AlertingAuthorization', () => { ], }, }); - const alertAuthorization = new AlertingAuthorization({ request, authorization, @@ -804,7 +1049,6 @@ describe('AlertingAuthorization', () => { exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Alert, { @@ -820,22 +1064,20 @@ describe('AlertingAuthorization', () => { }).toThrowErrorMatchingInlineSnapshot( `"Unauthorized to find a \\"myAppAlertType\\" alert for \\"myOtherApp\\""` ); - expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.logAuthorizationFailure).toHaveBeenCalledTimes(1); expect(auditLogger.logAuthorizationFailure.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", - "myAppAlertType", - 0, - "myOtherApp", - "find", - "alert", - ] - `); + Array [ + "some-user", + "myAppAlertType", + 0, + "myOtherApp", + "find", + "alert", + ] + `); }); - - test('creates an `ensureAlertTypeIsAuthorized` function which is no-op if type is authorized', async () => { + test('creates an `ensureRuleTypeIsAuthorized` function which is no-op if type is authorized', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< ReturnType @@ -870,7 +1112,6 @@ describe('AlertingAuthorization', () => { ], }, }); - const alertAuthorization = new AlertingAuthorization({ request, authorization, @@ -881,7 +1122,6 @@ describe('AlertingAuthorization', () => { exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureRuleTypeIsAuthorized } = await alertAuthorization.getFindAuthorizationFilter( AlertingAuthorizationEntity.Rule, { @@ -895,11 +1135,9 @@ describe('AlertingAuthorization', () => { expect(() => { ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); }).not.toThrow(); - expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); }); - test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => { const { authorization } = mockSecurity(); const checkPrivileges: jest.MockedFunction< @@ -948,7 +1186,6 @@ describe('AlertingAuthorization', () => { ], }, }); - const alertAuthorization = new AlertingAuthorization({ request, authorization, @@ -959,7 +1196,6 @@ describe('AlertingAuthorization', () => { exemptConsumerIds, }); alertTypeRegistry.list.mockReturnValue(setOfAlertTypes); - const { ensureRuleTypeIsAuthorized, logSuccessfulAuthorization, @@ -975,35 +1211,32 @@ describe('AlertingAuthorization', () => { ensureRuleTypeIsAuthorized('mySecondAppAlertType', 'myOtherApp', 'rule'); ensureRuleTypeIsAuthorized('myAppAlertType', 'myOtherApp', 'rule'); }).not.toThrow(); - expect(auditLogger.logAuthorizationSuccess).not.toHaveBeenCalled(); expect(auditLogger.logAuthorizationFailure).not.toHaveBeenCalled(); - logSuccessfulAuthorization(); - expect(auditLogger.logBulkAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.logBulkAuthorizationSuccess.mock.calls[0]).toMatchInlineSnapshot(` - Array [ - "some-user", Array [ + "some-user", Array [ - "myAppAlertType", - "myOtherApp", - ], - Array [ - "mySecondAppAlertType", - "myOtherApp", + Array [ + "myAppAlertType", + "myOtherApp", + ], + Array [ + "mySecondAppAlertType", + "myOtherApp", + ], ], - ], - 0, - "find", - "rule", - ] - `); + 0, + "find", + "rule", + ] + `); }); }); - describe('filterByAlertTypeAuthorization', () => { + describe('filterByRuleTypeAuthorization', () => { const myOtherAppAlertType: RegistryAlertType = { actionGroups: [], actionVariables: undefined, diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json index 136dc530aa1199..43292c6a543463 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.json @@ -47,7 +47,8 @@ } }, "params": { - "type": "flattened" + "type": "flattened", + "ignore_above": 4096 }, "scheduledTaskId": { "type": "keyword" diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index a340a940f4a3b6..fb0610dffb92e4 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -38,7 +38,12 @@ export const APM_FEATURE = { read: [], }, alerting: { - all: Object.values(AlertType), + rule: { + all: Object.values(AlertType), + }, + alert: { + all: Object.values(AlertType), + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -54,7 +59,12 @@ export const APM_FEATURE = { read: [], }, alerting: { - read: Object.values(AlertType), + rule: { + read: Object.values(AlertType), + }, + alert: { + read: Object.values(AlertType), + }, }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index cf5be4369f79e3..8d83f762e2023e 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -130,19 +130,20 @@ export class APMPlugin registerFeaturesUsage({ licensingPlugin: plugins.licensing }); + const { ruleDataService } = plugins.ruleRegistry; const getCoreStart = () => core.getStartServices().then(([coreStart]) => coreStart); const ready = once(async () => { - const componentTemplateName = plugins.ruleRegistry.getFullAssetName( + const componentTemplateName = ruleDataService.getFullAssetName( 'apm-mappings' ); - if (!plugins.ruleRegistry.isWriteEnabled()) { + if (!ruleDataService.isWriteEnabled()) { return; } - await plugins.ruleRegistry.createOrUpdateComponentTemplate({ + await ruleDataService.createOrUpdateComponentTemplate({ name: componentTemplateName, body: { template: { @@ -167,16 +168,14 @@ export class APMPlugin }, }); - await plugins.ruleRegistry.createOrUpdateIndexTemplate({ - name: plugins.ruleRegistry.getFullAssetName('apm-index-template'), + await ruleDataService.createOrUpdateIndexTemplate({ + name: ruleDataService.getFullAssetName('apm-index-template'), body: { index_patterns: [ - plugins.ruleRegistry.getFullAssetName('observability-apm*'), + ruleDataService.getFullAssetName('observability-apm*'), ], composed_of: [ - plugins.ruleRegistry.getFullAssetName( - TECHNICAL_COMPONENT_TEMPLATE_NAME - ), + ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME), componentTemplateName, ], }, @@ -188,7 +187,7 @@ export class APMPlugin }); const ruleDataClient = new RuleDataClient({ - alias: plugins.ruleRegistry.getFullAssetName('observability-apm'), + alias: ruleDataService.getFullAssetName('observability-apm'), getClusterClient: async () => { const coreStart = await getCoreStart(); return coreStart.elasticsearch.client.asInternalUser; diff --git a/x-pack/plugins/beats_management/common/config_schemas.ts b/x-pack/plugins/beats_management/common/config_schemas.ts deleted file mode 100644 index 95330655a77967..00000000000000 --- a/x-pack/plugins/beats_management/common/config_schemas.ts +++ /dev/null @@ -1,384 +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. - */ - -// Note: importing this is a temp thing. This file will be replaced with JSON from Beats/ES at some point -import { ConfigBlockSchema } from './domain_types'; - -export const configBlockSchemas: ConfigBlockSchema[] = [ - { - id: 'filebeat.inputs', - name: 'Filebeat Input', - version: 6.7, - allowOtherConfigs: true, - configs: [ - { - id: 'paths', - ui: { - label: 'Paths', - labelId: 'filebeatInputConfig.paths.ui.label', - type: 'multi-input', - helpText: 'filebeatInputConfig.paths.ui.helpText', - helpTextId: 'filebeatInputConfig.paths.ui.helpText', - placeholder: `first/path/to/file.json second/path/to/otherfile.json`, - }, - validation: 'isPaths', - error: 'filebeatInputConfig.paths.error', - errorId: 'filebeatInputConfig.paths.error', - required: true, - }, - ], - }, - { - id: 'filebeat.modules', - name: 'Filebeat Modules', - version: 6.7, - allowOtherConfigs: true, - configs: [ - { - id: '_sub_type', - ui: { - label: 'filebeatModuleConfig.module.ui.label', - labelId: 'filebeatModuleConfig.module.ui.label', - type: 'select', - }, - options: [ - { - value: 'apache2', - text: 'apache2', - }, - { - value: 'auditd', - text: 'auditd', - }, - { - value: 'elasticsearch', - text: 'elasticsearch', - }, - { - value: 'haproxy', - text: 'haproxy', - }, - { - value: 'icinga', - text: 'icinga', - }, - { - value: 'iis', - text: 'iis', - }, - { - value: 'kafka', - text: 'kafka', - }, - { - value: 'kibana', - text: 'kibana', - }, - { - value: 'logstash', - text: 'logstash', - }, - { - value: 'mongodb', - text: 'mongodb', - }, - { - value: 'mysql', - text: 'mysql', - }, - { - value: 'nginx', - text: 'nginx', - }, - { - value: 'osquery', - text: 'osquery', - }, - { - value: 'postgresql', - text: 'postgresql', - }, - { - value: 'redis', - text: 'redis', - }, - { - value: 'system', - text: 'system', - }, - { - value: 'traefik', - text: 'traefik', - }, - ], - error: 'filebeatModuleConfig.module.error', - errorId: 'filebeatModuleConfig.module.error', - required: true, - }, - ], - }, - { - id: 'metricbeat.modules', - name: 'Metricbeat Modules', - version: 6.7, - allowOtherConfigs: true, - configs: [ - { - id: '_sub_type', - ui: { - label: 'metricbeatModuleConfig.module.ui.label', - labelId: 'metricbeatModuleConfig.module.ui.label', - type: 'select', - }, - options: [ - { - value: 'aerospike', - text: 'aerospike', - }, - { - value: 'apache', - text: 'apache', - }, - { - value: 'ceph', - text: 'ceph', - }, - { - value: 'couchbase', - text: 'couchbase', - }, - { - value: 'docker', - text: 'docker', - }, - { - value: 'dropwizard', - text: 'dropwizard', - }, - { - value: 'elasticsearch', - text: 'elasticsearch', - }, - { - value: 'envoyproxy', - text: 'envoyproxy', - }, - { - value: 'etcd', - text: 'etcd', - }, - { - value: 'golang', - text: 'golang', - }, - { - value: 'graphite', - text: 'graphite', - }, - { - value: 'haproxy', - text: 'haproxy', - }, - { - value: 'http', - text: 'http', - }, - { - value: 'jolokia', - text: 'jolokia', - }, - { - value: 'kafka', - text: 'kafka', - }, - { - value: 'kibana', - text: 'kibana', - }, - { - value: 'kubernetes', - text: 'kubernetes', - }, - { - value: 'kvm', - text: 'kvm', - }, - { - value: 'logstash', - text: 'logstash', - }, - { - value: 'memcached', - text: 'memcached', - }, - { - value: 'mongodb', - text: 'mongodb', - }, - { - value: 'munin', - text: 'munin', - }, - { - value: 'mysql', - text: 'mysql', - }, - { - value: 'nginx', - text: 'nginx', - }, - { - value: 'php_fpm', - text: 'php_fpm', - }, - { - value: 'postgresql', - text: 'postgresql', - }, - { - value: 'prometheus', - text: 'prometheus', - }, - { - value: 'rabbitmq', - text: 'rabbitmq', - }, - { - value: 'redis', - text: 'redis', - }, - { - value: 'system', - text: 'system', - }, - { - value: 'traefik', - text: 'traefik', - }, - { - value: 'uwsgi', - text: 'uwsgi', - }, - { - value: 'vsphere', - text: 'vsphere', - }, - { - value: 'windows', - text: 'windows', - }, - { - value: 'zookeeper', - text: 'zookeeper', - }, - ], - error: 'metricbeatModuleConfig.module.error', - errorId: 'metricbeatModuleConfig.module.error', - required: true, - }, - { - id: 'hosts', - ui: { - label: 'metricbeatModuleConfig.hosts.ui.label', - labelId: 'metricbeatModuleConfig.hosts.ui.label', - type: 'multi-input', - helpText: 'metricbeatModuleConfig.hosts.ui.helpText', - helpTextId: 'metricbeatModuleConfig.hosts.ui.helpText', - placeholder: `somehost.local otherhost.local`, - }, - validation: 'isHosts', - error: 'metricbeatModuleConfig.hosts.error', - errorId: 'metricbeatModuleConfig.hosts.error', - required: false, - }, - { - id: 'period', - ui: { - label: 'metricbeatModuleConfig.period.ui.label', - labelId: 'metricbeatModuleConfig.period.ui.label', - type: 'input', - }, - defaultValue: '10s', - validation: 'isPeriod', - error: 'metricbeatModuleConfig.period.error', - errorId: 'metricbeatModuleConfig.period.error', - required: true, - }, - ], - }, - { - id: 'output', - name: 'Outputs', - allowOtherConfigs: true, - version: 6.7, - configs: [ - { - id: '_sub_type', - ui: { - label: 'outputConfig.output.ui.label', - labelId: 'outputConfig.output.ui.label', - type: 'select', - }, - options: [ - { - value: 'elasticsearch', - text: 'Elasticsearch', - }, - { - value: 'logstash', - text: 'Logstash', - }, - { - value: 'kafka', - text: 'Kafka', - }, - { - value: 'redis', - text: 'Redis', - }, - ], - error: 'outputConfig.output.error', - errorId: 'outputConfig.output.error', - required: true, - }, - { - id: 'hosts', - ui: { - label: 'outputConfig.hosts.ui.label', - labelId: 'outputConfig.hosts.ui.label', - type: 'multi-input', - }, - validation: 'isHosts', - error: 'outputConfig.hosts.error', - errorId: 'outputConfig.hosts.error', - parseValidResult: (v) => v.split('\n'), - }, - { - id: 'username', - ui: { - label: 'outputConfig.username.ui.label', - labelId: 'outputConfig.username.ui.label', - type: 'input', - }, - validation: 'isString', - error: 'outputConfig.username.error', - errorId: 'outputConfig.username.error', - }, - { - id: 'password', - ui: { - label: 'outputConfig.password.ui.label', - labelId: 'outputConfig.password.ui.label', - type: 'password', - }, - validation: 'isString', - error: 'outputConfig.password.error', - errorId: 'outputConfig.password.error', - }, - ], - }, -]; diff --git a/x-pack/plugins/beats_management/common/config_schemas_translations_map.ts b/x-pack/plugins/beats_management/common/config_schemas_translations_map.ts deleted file mode 100644 index ca2d48998ee062..00000000000000 --- a/x-pack/plugins/beats_management/common/config_schemas_translations_map.ts +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { ConfigBlockSchema } from './domain_types'; - -const supportedConfigLabelsMap = new Map([ - [ - 'filebeatInputConfig.paths.ui.label', - i18n.translate('xpack.beatsManagement.filebeatInputConfig.pathsLabel', { - defaultMessage: 'Paths', - }), - ], - [ - 'filebeatInputConfig.paths.ui.helpText', - i18n.translate('xpack.beatsManagement.filebeatInputConfig.pathsDescription', { - defaultMessage: 'Put each of the paths on a separate line', - }), - ], - [ - 'filebeatInputConfig.paths.error', - i18n.translate('xpack.beatsManagement.filebeatInputConfig.pathsErrorMessage', { - defaultMessage: 'One file path per line', - }), - ], - [ - 'filebeatInputConfig.other.ui.label', - i18n.translate('xpack.beatsManagement.filebeatInputConfig.otherConfigLabel', { - defaultMessage: 'Other Config', - }), - ], - [ - 'filebeatInputConfig.other.ui.helpText', - i18n.translate('xpack.beatsManagement.filebeatInputConfig.otherConfigDescription', { - defaultMessage: 'Use YAML format to specify other settings for the Filebeat Input', - }), - ], - [ - 'filebeatInputConfig.other.error', - i18n.translate('xpack.beatsManagement.filebeatInputConfig.otherConfigErrorMessage', { - defaultMessage: 'Use valid YAML format', - }), - ], - [ - 'filebeatModuleConfig.module.ui.label', - i18n.translate('xpack.beatsManagement.filebeatModuleConfig.moduleLabel', { - defaultMessage: 'Module', - }), - ], - [ - 'filebeatModuleConfig.module.error', - i18n.translate('xpack.beatsManagement.filebeatModuleConfig.moduleErrorMessage', { - defaultMessage: 'Please select a module', - }), - ], - [ - 'filebeatModuleConfig.other.ui.label', - i18n.translate('xpack.beatsManagement.filebeatModuleConfig.otherConfigLabel', { - defaultMessage: 'Other Config', - }), - ], - [ - 'filebeatModuleConfig.other.ui.helpText', - i18n.translate('xpack.beatsManagement.filebeatModuleConfig.moduleDescription', { - defaultMessage: 'Use YAML format to specify other settings for the Filebeat Module', - }), - ], - [ - 'filebeatModuleConfig.other.error', - i18n.translate('xpack.beatsManagement.filebeatModuleConfig.otherConfigErrorMessage', { - defaultMessage: 'Use valid YAML format', - }), - ], - - [ - 'metricbeatModuleConfig.module.ui.label', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.moduleLabel', { - defaultMessage: 'Module', - }), - ], - [ - 'metricbeatModuleConfig.module.error', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.moduleErrorMessage', { - defaultMessage: 'Please select a module', - }), - ], - [ - 'metricbeatModuleConfig.hosts.ui.label', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.hostsLabel', { - defaultMessage: 'Hosts', - }), - ], - [ - 'metricbeatModuleConfig.hosts.ui.helpText', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.hostsDescription', { - defaultMessage: 'Put each of the paths on a seperate line', - }), - ], - [ - 'metricbeatModuleConfig.hosts.error', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.hostsErrorMessage', { - defaultMessage: 'One file host per line', - }), - ], - [ - 'metricbeatModuleConfig.period.ui.label', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.periodLabel', { - defaultMessage: 'Period', - }), - ], - [ - 'metricbeatModuleConfig.period.error', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.periodErrorMessage', { - defaultMessage: 'Invalid Period, must be formatted as `10s` for 10 seconds', - }), - ], - [ - 'metricbeatModuleConfig.other.ui.label', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.otherConfigLabel', { - defaultMessage: 'Other Config', - }), - ], - [ - 'metricbeatModuleConfig.other.ui.helpText', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.otherConfigDescription', { - defaultMessage: 'Use YAML format to specify other settings for the Metricbeat Module', - }), - ], - [ - 'metricbeatModuleConfig.other.error', - i18n.translate('xpack.beatsManagement.metricbeatModuleConfig.otherConfigErrorMessage', { - defaultMessage: 'Use valid YAML format', - }), - ], - - [ - 'outputConfig.output.ui.label', - i18n.translate('xpack.beatsManagement.outputConfig.outputTypeLabel', { - defaultMessage: 'Output Type', - }), - ], - [ - 'outputConfig.output.error', - i18n.translate('xpack.beatsManagement.outputConfig.outputTypeErrorMessage', { - defaultMessage: 'Please select an output type', - }), - ], - [ - 'outputConfig.hosts.ui.label', - i18n.translate('xpack.beatsManagement.outputConfig.hostsLabel', { - defaultMessage: 'Hosts', - }), - ], - [ - 'outputConfig.hosts.error', - i18n.translate('xpack.beatsManagement.outputConfig.hostsErrorMessage', { - defaultMessage: 'One file host per line', - }), - ], - [ - 'outputConfig.username.ui.label', - i18n.translate('xpack.beatsManagement.outputConfig.usernameLabel', { - defaultMessage: 'Username', - }), - ], - [ - 'outputConfig.username.error', - i18n.translate('xpack.beatsManagement.outputConfig.usernameErrorMessage', { - defaultMessage: 'Unprocessable username', - }), - ], - [ - 'outputConfig.password.ui.label', - i18n.translate('xpack.beatsManagement.outputConfig.passwordLabel', { - defaultMessage: 'Password', - }), - ], - [ - 'outputConfig.password.error', - i18n.translate('xpack.beatsManagement.outputConfig.passwordErrorMessage', { - defaultMessage: 'Unprocessable password', - }), - ], - - [ - 'supportedConfigs.filebeat.input.text', - i18n.translate('xpack.beatsManagement.tagConfig.filebeatInputLabel', { - defaultMessage: 'Filebeat Input', - }), - ], - [ - 'supportedConfigs.filebeat.modules.text', - i18n.translate('xpack.beatsManagement.tagConfig.filebeatModuleLabel', { - defaultMessage: 'Filebeat Module', - }), - ], - [ - 'supportedConfigs.metricbeatModule.text', - i18n.translate('xpack.beatsManagement.tagConfig.metricbeatModuleLabel', { - defaultMessage: 'Metricbeat Module', - }), - ], - [ - 'supportedConfigs.output.text', - i18n.translate('xpack.beatsManagement.tagConfig.outputLabel', { - defaultMessage: 'Output', - }), - ], -]); - -export let translatedConfigs: ConfigBlockSchema[]; -export const translateConfigSchema = (schemas: ConfigBlockSchema[]) => { - if (translatedConfigs) { - return translatedConfigs; - } - - translatedConfigs = schemas.map((schema) => { - schema.name = supportedConfigLabelsMap.get(`supportedConfigs.${schema.id}.text`) || schema.name; - - schema.configs = schema.configs.map((configBlock) => { - if (configBlock.ui.label) { - configBlock.ui.label = - supportedConfigLabelsMap.get(configBlock.ui.labelId || '') || configBlock.ui.label; - } - if (configBlock.ui.helpText) { - configBlock.ui.helpText = - supportedConfigLabelsMap.get(configBlock.ui.helpTextId || '') || configBlock.ui.helpText; - } - if (configBlock.error) { - configBlock.error = - supportedConfigLabelsMap.get(configBlock.errorId || '') || configBlock.error; - } - return configBlock; - }); - return schema; - }); - - return translatedConfigs; -}; diff --git a/x-pack/plugins/beats_management/common/constants/index.ts b/x-pack/plugins/beats_management/common/constants/index.ts deleted file mode 100644 index ac4f89b639c22a..00000000000000 --- a/x-pack/plugins/beats_management/common/constants/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { UNIQUENESS_ENFORCING_TYPES } from './configuration_blocks'; -export { INDEX_NAMES } from './index_names'; -export { PLUGIN, MANAGEMENT_SECTION } from './plugin'; -export { LICENSES, REQUIRED_LICENSES, REQUIRED_ROLES } from './security'; -export { TABLE_CONFIG } from './table'; -export const BASE_PATH = '/management/ingest/beats_management'; diff --git a/x-pack/plugins/beats_management/common/constants/index_names.ts b/x-pack/plugins/beats_management/common/constants/index_names.ts deleted file mode 100644 index f0a00c5ca28913..00000000000000 --- a/x-pack/plugins/beats_management/common/constants/index_names.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const INDEX_NAMES = { - BEATS: '.management-beats', - EVENTS: '.management-beats-events-*', - EVENTS_ALIAS: '.management-beats-events', -}; - -export const POLICY_NAMES = { - EVENTS: '.beats-management-events-retention', -}; diff --git a/x-pack/plugins/beats_management/common/constants/security.ts b/x-pack/plugins/beats_management/common/constants/security.ts deleted file mode 100644 index af4ef129f54b40..00000000000000 --- a/x-pack/plugins/beats_management/common/constants/security.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const REQUIRED_ROLES = ['beats_admin']; -export const REQUIRED_LICENSES = ['standard', 'gold', 'trial', 'platinum', 'enterprise']; -export const LICENSES = ['oss', 'basic', 'standard', 'gold', 'trial', 'platinum', 'enterprise']; -export type LicenseType = - | 'oss' - | 'basic' - | 'trial' - | 'standard' - | 'gold' - | 'platinum' - | 'enterprise'; diff --git a/x-pack/plugins/beats_management/common/domain_types.ts b/x-pack/plugins/beats_management/common/domain_types.ts deleted file mode 100644 index f7efb77f0dc395..00000000000000 --- a/x-pack/plugins/beats_management/common/domain_types.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { configBlockSchemas } from './config_schemas'; -import { DateFromString } from './io_ts_types'; - -// Here we create the runtime check for a generic, unknown beat config type. -// We can also pass in optional params to create spacific runtime checks that -// can be used to validate blocs on the API and UI -export const createConfigurationBlockInterface = ( - configType: t.LiteralType | t.KeyofC> = t.keyof( - Object.fromEntries(configBlockSchemas.map((s) => [s.id, null])) as Record - ), - beatConfigInterface: t.Mixed = t.Dictionary -) => - t.interface( - { - id: t.union([t.undefined, t.string]), - type: configType, - description: t.union([t.undefined, t.string]), - tag: t.string, - config: beatConfigInterface, - last_updated: t.union([t.undefined, t.number]), - }, - 'ConfigBlock' - ); -const BaseConfigurationBlock = createConfigurationBlockInterface(); -export interface ConfigurationBlock - extends Pick< - t.TypeOf, - Exclude, 'id'> - > { - id: string; -} - -export interface CMBeat { - id: string; - status?: BeatEvent; - enrollment_token: string; - active: boolean; - access_token?: string; - verified_on?: string; - type: string; - version?: string; - host_ip: string; - host_name: string; - ephemeral_id?: string; - last_checkin?: Date; - event_rate?: string; - local_configuration_yml?: string; - tags: string[]; - central_configuration_yml?: string; - metadata?: {}; - name?: string; - last_updated: number; -} - -export interface ConfigBlockSchema { - id: string; - name: string; - version: number; - allowOtherConfigs?: boolean; - configs: BeatConfigSchema[]; -} - -export interface BeatConfigSchema { - id: string; - ui: { - label: string; - labelId?: string; - type: 'input' | 'multi-input' | 'select' | 'code' | 'password'; - helpText?: string; - helpTextId?: string; - placeholder?: string; - }; - options?: Array<{ value: string; text: string }>; - validation?: 'isHosts' | 'isString' | 'isPeriod' | 'isPath' | 'isPaths' | 'isYaml'; - error: string; - errorId: string; - defaultValue?: string; - required?: boolean; - parseValidResult?: (value: any) => any; -} - -export const RuntimeBeatTag = t.interface( - { - id: t.union([t.undefined, t.string]), - name: t.string, - color: t.string, - hasConfigurationBlocksTypes: t.array(t.string), - }, - 'CMBeat' -); -export interface BeatTag - extends Pick< - t.TypeOf, - Exclude, 'id'> - > { - id: string; - // Used by the UI and api when a tag exists but is an invalid option - disabled?: boolean; -} - -export const RuntimeBeatEvent = t.interface( - { - type: t.union([t.literal('STATE'), t.literal('ERROR')]), - beat: t.union([t.undefined, t.string]), - timestamp: DateFromString, - event: t.type({ - type: t.union([ - t.literal('RUNNING'), - t.literal('STARTING'), - t.literal('IN_PROGRESS'), - t.literal('CONFIG'), - t.literal('FAILED'), - t.literal('STOPPED'), - ]), - message: t.string, - uuid: t.union([t.undefined, t.string]), - }), - }, - 'BeatEvent' -); -export interface BeatEvent - extends Pick< - t.TypeOf, - Exclude, 'timestamp'> - > { - beat: string; - timestamp: Date; -} diff --git a/x-pack/plugins/beats_management/common/index.ts b/x-pack/plugins/beats_management/common/index.ts deleted file mode 100644 index a7dde2298054fb..00000000000000 --- a/x-pack/plugins/beats_management/common/index.ts +++ /dev/null @@ -1,23 +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 { schema, TypeOf } from '@kbn/config-schema'; - -const DEFAULT_ENROLLMENT_TOKENS_TTL_S = 10 * 60; // 10 minutes - -export const beatsManagementConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - defaultUserRoles: schema.arrayOf(schema.string(), { defaultValue: ['superuser'] }), - encryptionKey: schema.string({ defaultValue: 'xpack_beats_default_encryptionKey' }), - enrollmentTokensTtlInSeconds: schema.number({ - min: 1, - max: 10 * 60 * 14, // No more then 2 weeks for security reasons, - defaultValue: DEFAULT_ENROLLMENT_TOKENS_TTL_S, - }), -}); - -export type BeatsManagementConfigType = TypeOf; diff --git a/x-pack/plugins/beats_management/common/io_ts_types.ts b/x-pack/plugins/beats_management/common/io_ts_types.ts deleted file mode 100644 index b1abd3d8b6b3bb..00000000000000 --- a/x-pack/plugins/beats_management/common/io_ts_types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { isRight } from 'fp-ts/lib/Either'; - -class DateFromStringType extends t.Type { - public readonly _tag: 'DateFromISOStringType' = 'DateFromISOStringType'; - constructor() { - super( - 'DateFromString', - (u): u is Date => u instanceof Date, - (u, c) => { - const validation = t.string.validate(u, c); - if (!isRight(validation)) { - return validation as any; - } else { - const s = validation.right; - const d = new Date(s); - return isNaN(d.getTime()) ? t.failure(s, c) : t.success(d); - } - }, - (a) => a.toISOString() - ); - } -} -// eslint-disable-next-line -export interface DateFromString extends DateFromStringType {} - -export const DateFromString: DateFromString = new DateFromStringType(); diff --git a/x-pack/plugins/beats_management/common/return_types.ts b/x-pack/plugins/beats_management/common/return_types.ts deleted file mode 100644 index c498038d6a21da..00000000000000 --- a/x-pack/plugins/beats_management/common/return_types.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface BaseReturnType { - error?: { - message: string; - code?: number; - }; - success: boolean; -} - -export interface ReturnTypeCreate extends BaseReturnType { - item: T; - action: 'created'; -} - -export interface ReturnTypeUpdate extends BaseReturnType { - item: T; - action: 'updated'; -} - -export interface ReturnTypeBulkCreate extends BaseReturnType { - results: Array<{ - item: T; - success: boolean; - action: 'created'; - error?: { - message: string; - code?: number; - }; - }>; -} - -export interface ReturnTypeBulkDelete extends BaseReturnType { - results: Array<{ - success: boolean; - action: 'deleted'; - error?: { - message: string; - code?: number; - }; - }>; -} - -// upsert -export interface ReturnTypeUpsert extends BaseReturnType { - item: T; - action: 'created' | 'updated'; -} - -// upsert bulk -export interface ReturnTypeBulkUpsert extends BaseReturnType { - results: Array<{ - success: boolean; - action: 'created' | 'updated'; - error?: { - message: string; - code?: number; - }; - }>; -} - -// list -export interface ReturnTypeList extends BaseReturnType { - list: T[]; - page: number; - total: number; -} - -// get -export interface ReturnTypeGet extends BaseReturnType { - item: T; -} - -export interface ReturnTypeBulkGet extends BaseReturnType { - items: T[]; -} - -// e.g. -// { -// result: { -// username: { valid: true }, -// password: { valid: false, error: 'something' }, -// hosts: [ -// { valid: false }, { valid: true }, -// ] -// } -// } - -// bulk action -- e.g. assign tags to beats -export interface ReturnTypeBulkAction extends BaseReturnType { - results?: Array<{ - success: boolean; - result?: { - [key: string]: any; - }; - error?: { - message: string; - code?: number; - }; - }>; -} diff --git a/x-pack/plugins/beats_management/jest.config.js b/x-pack/plugins/beats_management/jest.config.js deleted file mode 100644 index be070350450038..00000000000000 --- a/x-pack/plugins/beats_management/jest.config.js +++ /dev/null @@ -1,12 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/x-pack/plugins/beats_management'], -}; diff --git a/x-pack/plugins/beats_management/kibana.json b/x-pack/plugins/beats_management/kibana.json deleted file mode 100644 index c1070eedf07a62..00000000000000 --- a/x-pack/plugins/beats_management/kibana.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "beatsManagement", - "configPath": ["xpack", "beats_management"], - "ui": true, - "server": true, - "version": "kibana", - "requiredPlugins": [ - "data", - "licensing", - "management", - "features" - ], - "optionalPlugins": [ - "security" - ] -} diff --git a/x-pack/plugins/beats_management/public/application.tsx b/x-pack/plugins/beats_management/public/application.tsx deleted file mode 100644 index 5a9b0a768856ed..00000000000000 --- a/x-pack/plugins/beats_management/public/application.tsx +++ /dev/null @@ -1,89 +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 euiVars from '@elastic/eui/dist/eui_theme_light.json'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import { Router } from 'react-router-dom'; -import { ThemeProvider } from 'styled-components'; -import { Provider as UnstatedProvider, Subscribe } from 'unstated'; -import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; -import { Background } from './components/layouts/background'; -import { BreadcrumbProvider } from './components/navigation/breadcrumb'; -import { Breadcrumb } from './components/navigation/breadcrumb/breadcrumb'; -import { BeatsContainer } from './containers/beats'; -import { TagsContainer } from './containers/tags'; -import { FrontendLibs } from './lib/types'; -import { AppRouter } from './router'; -import { services } from './kbn_services'; -import { ManagementAppMountParams } from '../../../../src/plugins/management/public'; - -export const renderApp = ({ element, history }: ManagementAppMountParams, libs: FrontendLibs) => { - ReactDOM.render( - - - - - - - {(beats: BeatsContainer, tags: TagsContainer) => ( - - - -

- - - - ), - }} - /> -

-
- - -
- )} -
-
-
-
-
-
, - element - ); - - return () => { - ReactDOM.unmountComponentAtNode(element); - }; -}; diff --git a/x-pack/plugins/beats_management/public/bootstrap.tsx b/x-pack/plugins/beats_management/public/bootstrap.tsx deleted file mode 100644 index b5bdd39fa0817e..00000000000000 --- a/x-pack/plugins/beats_management/public/bootstrap.tsx +++ /dev/null @@ -1,69 +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 { FrontendLibs } from './lib/types'; -import { compose } from './lib/compose/kibana'; - -import { setServices } from './kbn_services'; -import { ManagementSetup } from '../../../../src/plugins/management/public'; -import { SecurityPluginSetup } from '../../security/public'; -import { CoreSetup } from '../../../../src/core/public'; -import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { LicensingPluginSetup } from '../../licensing/public'; -import { BeatsManagementConfigType } from '../common'; -import { MANAGEMENT_SECTION } from '../common/constants'; - -async function startApp(libs: FrontendLibs, core: CoreSetup) { - const startServices = await core.getStartServices(); - - if (startServices[0].http.anonymousPaths.isAnonymous(window.location.pathname)) { - return; - } - // Can't run until the `start` lifecycle, so we wait for start services to resolve above before calling this. - await libs.framework.waitUntilFrameworkReady(); - - const capabilities = startServices[0].application.capabilities; - const hasBeatsCapability = capabilities.management.ingest?.[MANAGEMENT_SECTION] ?? false; - - if (libs.framework.licenseIsAtLeast('standard') && hasBeatsCapability) { - const mount = async (params: any) => { - const [coreStart, pluginsStart] = await core.getStartServices(); - setServices(coreStart, pluginsStart, params); - const { renderApp } = await import('./application'); - return renderApp(params, libs); - }; - - libs.framework.registerManagementUI(mount); - } -} - -interface SetupDeps { - management: ManagementSetup; - licensing: LicensingPluginSetup; - security?: SecurityPluginSetup; -} - -interface StartDeps { - data: DataPublicPluginStart; -} - -export const bootstrap = ( - core: CoreSetup, - plugins: SetupDeps, - config: BeatsManagementConfigType, - version: string -) => { - startApp( - compose({ - core, - config, - version, - ...plugins, - }), - core - ); -}; diff --git a/x-pack/plugins/beats_management/public/components/autocomplete_field/index.tsx b/x-pack/plugins/beats_management/public/components/autocomplete_field/index.tsx deleted file mode 100644 index 320296274e262b..00000000000000 --- a/x-pack/plugins/beats_management/public/components/autocomplete_field/index.tsx +++ /dev/null @@ -1,290 +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 { - EuiFieldSearch, - EuiFieldSearchProps, - EuiOutsideClickDetector, - EuiPanel, -} from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; - -import { composeStateUpdaters } from '../../utils/typed_react'; -import { SuggestionItem } from './suggestion_item'; - -interface AutocompleteFieldProps { - isLoadingSuggestions: boolean; - isValid: boolean; - loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; - onSubmit?: (value: string) => void; - onChange?: (value: string) => void; - placeholder?: string; - suggestions: QuerySuggestion[]; - value: string; -} - -interface AutocompleteFieldState { - areSuggestionsVisible: boolean; - selectedIndex: number | null; -} - -export class AutocompleteField extends React.Component< - AutocompleteFieldProps, - AutocompleteFieldState -> { - public readonly state: AutocompleteFieldState = { - areSuggestionsVisible: false, - selectedIndex: null, - }; - - private inputElement: HTMLInputElement | null = null; - - public render() { - const { suggestions, isLoadingSuggestions, isValid, placeholder, value } = this.props; - const { areSuggestionsVisible, selectedIndex } = this.state; - - return ( - - - - {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( - - {suggestions.map((suggestion, suggestionIndex) => ( - - ))} - - ) : null} - - - ); - } - - public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { - const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; - const hasNewValue = prevProps.value !== this.props.value; - - if (hasNewValue) { - this.updateSuggestions(); - } - - if (hasNewSuggestions) { - this.showSuggestions(); - } - } - - private handleChangeInputRef = (element: HTMLInputElement | null) => { - this.inputElement = element; - }; - - private handleChange = (evt: React.ChangeEvent) => { - this.changeValue(evt.currentTarget.value); - }; - - private handleKeyDown = (evt: React.KeyboardEvent) => { - const { suggestions } = this.props; - - switch (evt.key) { - case 'ArrowUp': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState( - composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) - ); - } - break; - case 'ArrowDown': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); - } else { - this.updateSuggestions(); - } - break; - case 'Enter': - evt.preventDefault(); - if (this.state.selectedIndex !== null) { - this.applySelectedSuggestion(); - } else { - this.submit(); - } - break; - case 'Escape': - evt.preventDefault(); - this.setState(withSuggestionsHidden); - break; - } - }; - - private handleKeyUp = (evt: React.KeyboardEvent) => { - switch (evt.key) { - case 'ArrowLeft': - case 'ArrowRight': - case 'Home': - case 'End': - this.updateSuggestions(); - break; - } - }; - - private selectSuggestionAt = (index: number) => () => { - this.setState(withSuggestionAtIndexSelected(index)); - }; - - private applySelectedSuggestion = () => { - if (this.state.selectedIndex !== null) { - this.applySuggestionAt(this.state.selectedIndex)(); - } - }; - - private applySuggestionAt = (index: number) => () => { - const { value, suggestions } = this.props; - const selectedSuggestion = suggestions[index]; - - if (!selectedSuggestion) { - return; - } - - const newValue = - value.substr(0, selectedSuggestion.start) + - selectedSuggestion.text + - value.substr(selectedSuggestion.end); - - this.setState(withSuggestionsHidden); - this.changeValue(newValue); - this.focusInputElement(); - }; - - private changeValue = (value: string) => { - const { onChange } = this.props; - if (onChange) { - onChange(value); - } - }; - - private focusInputElement = () => { - if (this.inputElement) { - this.inputElement.focus(); - } - }; - - private showSuggestions = () => { - this.setState(withSuggestionsVisible); - }; - - private hideSuggestions = () => { - this.setState(withSuggestionsHidden); - }; - - private submit = () => { - const { isValid, onSubmit, value } = this.props; - - if (isValid && onSubmit) { - onSubmit(value); - } - - this.setState(withSuggestionsHidden); - }; - - private updateSuggestions = (value?: string) => { - const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; - this.props.loadSuggestions(value || this.props.value, inputCursorPosition, 10); - }; -} - -const withPreviousSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length - : Math.max(props.suggestions.length - 1, 0), -}); - -const withNextSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + 1) % props.suggestions.length - : 0, -}); - -const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length - ? suggestionIndex - : 0, -}); - -const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: true, -}); - -const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: false, - selectedIndex: null, -}); - -const FixedEuiFieldSearch: React.FC< - React.InputHTMLAttributes & - EuiFieldSearchProps & { - inputRef?: (element: HTMLInputElement | null) => void; - onSearch: (value: string) => void; - } -> = EuiFieldSearch as any; - -const AutocompleteContainer = styled.div` - position: relative; -`; - -const SuggestionsPanel = styled(EuiPanel).attrs(() => ({ - paddingSize: 'none', - hasShadow: true, -}))` - position: absolute; - width: 100%; - margin-top: 2px; - overflow: hidden; - z-index: 1000; -`; diff --git a/x-pack/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx deleted file mode 100644 index b870982d63d22d..00000000000000 --- a/x-pack/plugins/beats_management/public/components/autocomplete_field/suggestion_item.tsx +++ /dev/null @@ -1,119 +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 { EuiIcon } from '@elastic/eui'; -import { tint } from 'polished'; -import React from 'react'; -import styled from 'styled-components'; - -import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; - -interface SuggestionItemProps { - isSelected?: boolean; - onClick?: React.MouseEventHandler; - onMouseEnter?: React.MouseEventHandler; - suggestion: QuerySuggestion; -} - -export const SuggestionItem: React.FC = (props) => { - const { isSelected, onClick, onMouseEnter, suggestion } = props; - - return ( - - - - - {suggestion.text} - {suggestion.description} - - ); -}; - -SuggestionItem.defaultProps = { - isSelected: false, -}; - -const SuggestionItemContainer = styled.div<{ - isSelected?: boolean; -}>` - display: flex; - flex-direction: row; - font-size: ${(props) => props.theme.eui.default.euiFontSizeS}; - height: ${(props) => props.theme.eui.default.euiSizeXl}; - white-space: nowrap; - background-color: ${(props) => - props.isSelected ? props.theme.eui.default.euiColorLightestShade : 'transparent'}; -`; - -const SuggestionItemField = styled.div` - align-items: center; - cursor: pointer; - display: flex; - flex-direction: row; - height: ${(props) => props.theme.eui.default.euiSizeXl}; - padding: ${(props) => props.theme.eui.default.euiSizeXs}; -`; - -const SuggestionItemIconField = styled(SuggestionItemField)<{ suggestionType: string }>` - background-color: ${(props) => { - return tint(0.1, getEuiIconColor(props.theme, props.suggestionType)); - }}; - color: ${(props) => { - return getEuiIconColor(props.theme, props.suggestionType); - }}; - flex: 0 0 auto; - justify-content: center; - width: ${(props) => props.theme.eui.default.euiSizeXl}; -`; - -const SuggestionItemTextField = styled(SuggestionItemField)` - flex: 2 0 0; - font-family: ${(props) => props.theme.eui.default.euiCodeFontFamily}; -`; - -const SuggestionItemDescriptionField = styled(SuggestionItemField)` - flex: 3 0 0; - p { - display: inline; - span { - font-family: ${(props) => props.theme.eui.default.euiCodeFontFamily}; - } - } -`; - -const getEuiIconType = (suggestionType: string) => { - switch (suggestionType) { - case 'field': - return 'kqlField'; - case 'value': - return 'kqlValue'; - case 'recentSearch': - return 'search'; - case 'conjunction': - return 'kqlSelector'; - case 'operator': - return 'kqlOperand'; - default: - return 'empty'; - } -}; - -const getEuiIconColor = (theme: any, suggestionType: string): string => { - switch (suggestionType) { - case 'field': - return theme.eui.default.euiColorVis7; - case 'value': - return theme.eui.default.euiColorVis0; - case 'operator': - return theme.eui.default.euiColorVis1; - case 'conjunction': - return theme.eui.default.euiColorVis2; - case 'recentSearch': - default: - return theme.eui.default.euiColorMediumShade; - } -}; diff --git a/x-pack/plugins/beats_management/public/components/config_list.tsx b/x-pack/plugins/beats_management/public/components/config_list.tsx deleted file mode 100644 index 44a581a8667352..00000000000000 --- a/x-pack/plugins/beats_management/public/components/config_list.tsx +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// @ts-ignore -import { EuiBasicTable, EuiLink } from '@elastic/eui'; -import { InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React from 'react'; -import { configBlockSchemas } from '../../common/config_schemas'; -import { translateConfigSchema } from '../../common/config_schemas_translations_map'; -import { ConfigurationBlock } from '../../common/domain_types'; - -interface ComponentProps { - configs: { - error?: string | undefined; - list: ConfigurationBlock[]; - page: number; - total: number; - }; - onConfigClick: (action: 'edit' | 'delete', config: ConfigurationBlock) => any; - onTableChange: (index: number, size: number) => void; - intl: InjectedIntl; -} -const pagination = { - pageSize: 5, - hidePerPageOptions: true, -}; - -const ConfigListUi: React.FC = (props) => ( - { - if (props.onTableChange) { - props.onTableChange(table.page.index, table.page.size); - } - }} - columns={[ - { - field: 'type', - name: props.intl.formatMessage({ - id: 'xpack.beatsManagement.tagTable.typeColumnName', - defaultMessage: 'Type', - }), - truncateText: false, - render: (type: string, config: ConfigurationBlock) => { - const translatedConfig = translateConfigSchema(configBlockSchemas).find( - (sc) => sc.id === type - ); - - return ( - props.onConfigClick('edit', config)}> - {translatedConfig ? translatedConfig.name : type} - - ); - }, - }, - { - field: 'module', - name: props.intl.formatMessage({ - id: 'xpack.beatsManagement.tagTable.moduleColumnName', - defaultMessage: 'Module', - }), - truncateText: false, - render: (value: string, config: ConfigurationBlock) => { - return ( - config.config._sub_type || - props.intl.formatMessage({ - id: 'xpack.beatsManagement.tagTable.moduleColumn.notAvailibaleLabel', - defaultMessage: 'N/A', - }) - ); - }, - }, - { - field: 'description', - name: props.intl.formatMessage({ - id: 'xpack.beatsManagement.tagTable.descriptionColumnName', - defaultMessage: 'Description', - }), - }, - { - name: props.intl.formatMessage({ - id: 'xpack.beatsManagement.tagTable.actionsColumnName', - defaultMessage: 'Actions', - }), - actions: [ - { - name: props.intl.formatMessage({ - id: 'xpack.beatsManagement.tagTable.actions.removeButtonAriaLabel', - defaultMessage: 'Remove', - }), - description: props.intl.formatMessage({ - id: 'xpack.beatsManagement.tagTable.actions.removeTooltip', - defaultMessage: 'Remove this config from tag', - }), - type: 'icon', - icon: 'trash', - onClick: (item: ConfigurationBlock) => props.onConfigClick('delete', item), - }, - ], - }, - ]} - /> -); - -export const ConfigList = injectI18n(ConfigListUi); diff --git a/x-pack/plugins/beats_management/public/components/enroll_beats.tsx b/x-pack/plugins/beats_management/public/components/enroll_beats.tsx deleted file mode 100644 index 4c85509f13256e..00000000000000 --- a/x-pack/plugins/beats_management/public/components/enroll_beats.tsx +++ /dev/null @@ -1,292 +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 { - EuiBasicTable, - EuiButton, - EuiCodeBlock, - EuiCopy, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiModalBody, - // @ts-ignore - EuiSelect, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { upperFirst } from 'lodash'; -import React from 'react'; -import { CMBeat } from '../../common/domain_types'; - -interface ComponentProps { - /** Such as kibanas basePath, for use to generate command */ - frameworkBasePath?: string; - enrollmentToken?: string; - getBeatWithToken(token: string): Promise; - createEnrollmentToken(): Promise; - onBeatEnrolled(enrolledBeat: CMBeat): void; -} - -interface ComponentState { - enrolledBeat: CMBeat | null; - hasPolledForBeat: boolean; - command: string; - beatType: string; -} - -export class EnrollBeat extends React.Component { - private pinging = false; - constructor(props: ComponentProps) { - super(props); - - this.state = { - enrolledBeat: null, - hasPolledForBeat: false, - command: 'sudo {{beatType}}', - beatType: 'filebeat', - }; - } - public pingForBeatWithToken = async (token: string): Promise => { - try { - const beats = await this.props.getBeatWithToken(token); - - if (!beats) { - throw new Error('no beats'); - } - return beats; - } catch (err) { - if (this.pinging) { - const timeout = (ms: number) => new Promise((res) => setTimeout(res, ms)); - await timeout(5000); - return await this.pingForBeatWithToken(token); - } - } - }; - public async componentDidMount() { - if (!this.props.enrollmentToken) { - await this.props.createEnrollmentToken(); - } - } - public waitForTokenToEnrollBeat = async () => { - if (this.pinging || !this.props.enrollmentToken) { - return; - } - this.pinging = true; - const enrolledBeat = (await this.pingForBeatWithToken(this.props.enrollmentToken)) as CMBeat; - - this.setState({ - enrolledBeat, - }); - this.props.onBeatEnrolled(enrolledBeat); - this.pinging = false; - }; - public render() { - if (!this.props.enrollmentToken && !this.state.enrolledBeat) { - return null; - } - if (this.props.enrollmentToken && !this.state.enrolledBeat) { - this.waitForTokenToEnrollBeat(); - } - const cmdText = `${this.state.command - .replace('{{beatType}}', this.state.beatType) - .replace('{{beatTypeInCaps}}', upperFirst(this.state.beatType))} enroll ${ - window.location.protocol - }//${window.location.host}${this.props.frameworkBasePath} ${this.props.enrollmentToken}`; - - return ( - - {!this.state.enrolledBeat && ( - - - - - - -

- -

-
-
-
- this.setState({ beatType: e.target.value })} - fullWidth={true} - /> -
-
- -
-
- - - - - -

- -

-
-
-
- {{beatType}}.exe`, - text: 'Windows', - }, - { - value: `./{{beatType}}`, - text: 'MacOS', - }, - ]} - onChange={(e: any) => this.setState({ command: e.target.value })} - fullWidth={true} - /> -
-
-
-
- {this.state.command && ( - - - - - -

- -

-
-
- - - {(copy: any) => ( - - - - )} - - -
- -
- - {`$ ${cmdText}`} -
- - - - - - - - -

- -

-
-
-
-
-
-
- -
-
- )} -
- )} - {this.state.enrolledBeat && ( - - -
-
-
- - ), - sortable: false, - }, - { - field: 'version', - name: ( - - ), - sortable: false, - }, - { - field: 'host_name', - name: ( - - ), - sortable: false, - }, - ]} - /> -
-
-
- )} -
- ); - } -} diff --git a/x-pack/plugins/beats_management/public/components/inputs/code_editor.tsx b/x-pack/plugins/beats_management/public/components/inputs/code_editor.tsx deleted file mode 100644 index 9116725bfe06ab..00000000000000 --- a/x-pack/plugins/beats_management/public/components/inputs/code_editor.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// @ts-ignore -import { CommonProps, EuiCodeEditor, EuiCodeEditorProps, EuiFormRow } from '@elastic/eui'; -// @ts-ignore -import { FormsyInputProps, withFormsy } from 'formsy-react'; -import React, { Component, InputHTMLAttributes } from 'react'; - -interface ComponentProps extends FormsyInputProps, CommonProps, EuiCodeEditorProps { - instantValidation: boolean; - label: string; - isReadOnly: boolean; - mode: 'javascript' | 'yaml'; - errorText: string; - fullWidth: boolean; - helpText: React.ReactElement; - compressed: boolean; - onChange(value: string): void; - onBlur(): void; -} - -interface ComponentState { - allowError: boolean; -} - -class CodeEditor extends Component< - InputHTMLAttributes & ComponentProps, - ComponentState -> { - public static defaultProps = { - passRequiredToField: true, - }; - - public state = { allowError: false }; - - public componentDidMount() { - const { defaultValue, setValue } = this.props; - setValue(defaultValue || ''); - } - - public UNSAFE_componentWillReceiveProps(nextProps: ComponentProps) { - if (nextProps.isFormSubmitted()) { - this.showError(); - } - } - - public handleChange = (value: string) => { - this.props.setValue(value); - if (this.props.onChange) { - this.props.onChange(value); - } - if (this.props.instantValidation) { - this.showError(); - } - }; - - public handleBlur = () => { - this.showError(); - if (this.props.onBlur) { - this.props.onBlur(); - } - }; - - public showError = () => this.setState({ allowError: true }); - - public render() { - const { - name, - id, - label, - isReadOnly, - isValid, - getValue, - isPristine, - getErrorMessage, - mode, - fullWidth, - className, - helpText, - } = this.props; - - const { allowError } = this.state; - const error = !isPristine() && !isValid() && allowError; - - return ( - - - - ); - } -} - -export const FormsyEuiCodeEditor = withFormsy(CodeEditor); diff --git a/x-pack/plugins/beats_management/public/components/inputs/index.ts b/x-pack/plugins/beats_management/public/components/inputs/index.ts deleted file mode 100644 index 58f5ba4c81f71c..00000000000000 --- a/x-pack/plugins/beats_management/public/components/inputs/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { FormsyEuiCodeEditor } from './code_editor'; -export { FormsyEuiFieldText } from './input'; -export { FormsyEuiPasswordText } from './password_input'; -export { FormsyEuiMultiFieldText } from './multi_input'; -export { FormsyEuiSelect } from './select'; diff --git a/x-pack/plugins/beats_management/public/components/inputs/input.tsx b/x-pack/plugins/beats_management/public/components/inputs/input.tsx deleted file mode 100644 index 3fb18e1b218b43..00000000000000 --- a/x-pack/plugins/beats_management/public/components/inputs/input.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFieldText, EuiFormRow } from '@elastic/eui'; -import { CommonProps } from '@elastic/eui/src/components/common'; -import { FormsyInputProps, withFormsy } from 'formsy-react'; -import React, { Component, InputHTMLAttributes } from 'react'; - -interface ComponentProps - extends FormsyInputProps, - CommonProps, - Omit, 'onChange' | 'onBlur'> { - instantValidation?: boolean; - label: string; - errorText: string; - fullWidth: boolean; - helpText: React.ReactElement; - compressed: boolean; - onChange?(e: React.ChangeEvent, value: any): void; - onBlur?(e: React.ChangeEvent, value: any): void; -} - -interface ComponentState { - allowError: boolean; -} - -class FieldText extends Component< - InputHTMLAttributes & ComponentProps, - ComponentState -> { - public static defaultProps = { - passRequiredToField: true, - }; - - public state = { allowError: false }; - - public componentDidMount() { - const { defaultValue, setValue } = this.props; - if (defaultValue) { - setValue(defaultValue); - } - } - - public UNSAFE_componentWillReceiveProps(nextProps: ComponentProps) { - if (nextProps.isFormSubmitted()) { - this.showError(); - } - } - - public handleChange = (e: React.ChangeEvent) => { - const { value } = e.currentTarget; - this.props.setValue(value); - if (this.props.onChange) { - this.props.onChange(e, e.currentTarget.value); - } - if (this.props.instantValidation) { - this.showError(); - } - }; - - public handleBlur = (e: React.ChangeEvent) => { - this.showError(); - if (this.props.onBlur) { - this.props.onBlur(e, e.currentTarget.value); - } - }; - - public showError = () => this.setState({ allowError: true }); - - public render() { - const { - name, - id, - required, - label, - getValue, - isValid, - isPristine, - getErrorMessage, - fullWidth, - className, - disabled, - helpText, - placeholder, - } = this.props; - - const { allowError } = this.state; - const error = !isPristine() && !isValid() && allowError; - - return ( - - - - ); - } -} - -export const FormsyEuiFieldText = withFormsy(FieldText); diff --git a/x-pack/plugins/beats_management/public/components/inputs/multi_input.tsx b/x-pack/plugins/beats_management/public/components/inputs/multi_input.tsx deleted file mode 100644 index ffc208f6f94fd6..00000000000000 --- a/x-pack/plugins/beats_management/public/components/inputs/multi_input.tsx +++ /dev/null @@ -1,122 +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 { EuiFormRow, EuiTextArea, EuiTextAreaProps } from '@elastic/eui'; -import { CommonProps } from '@elastic/eui/src/components/common'; -// @ts-ignore -import { FormsyInputProps, withFormsy } from 'formsy-react'; -import React, { Component, InputHTMLAttributes } from 'react'; - -interface ComponentProps - extends FormsyInputProps, - CommonProps, - Omit { - instantValidation: boolean; - label: string; - errorText: string; - fullWidth: boolean; - helpText: React.ReactElement; - compressed: boolean; - onChange(e: React.ChangeEvent, value: any): void; - onBlur(e: React.ChangeEvent, value: any): void; -} - -interface ComponentState { - allowError: boolean; -} - -class MultiFieldText extends Component< - InputHTMLAttributes & ComponentProps, - ComponentState -> { - public static defaultProps = { - passRequiredToField: true, - }; - - public state = { allowError: false }; - - public componentDidMount() { - const { defaultValue, setValue } = this.props; - - if (defaultValue) { - setValue(defaultValue); - } - } - - public UNSAFE_componentWillReceiveProps(nextProps: ComponentProps) { - if (nextProps.isFormSubmitted()) { - this.showError(); - } - } - - public handleChange = (e: React.ChangeEvent) => { - const value = e.currentTarget.value.split('\n'); - this.props.setValue(value); - if (this.props.onChange) { - this.props.onChange(e, value); - } - if (this.props.instantValidation) { - this.showError(); - } - }; - - public handleBlur = (e: React.ChangeEvent) => { - this.showError(); - if (this.props.onBlur) { - this.props.onBlur(e, e.currentTarget.value); - } - }; - - public showError = () => this.setState({ allowError: true }); - - public render() { - const { - name, - id, - required, - label, - getValue, - isValid, - isPristine, - getErrorMessage, - fullWidth, - className, - disabled, - helpText, - placeholder, - } = this.props; - - const { allowError } = this.state; - const error = !isPristine() && !isValid() && allowError; - - return ( - - - - ); - } -} - -export const FormsyEuiMultiFieldText = withFormsy(MultiFieldText); diff --git a/x-pack/plugins/beats_management/public/components/inputs/password_input.tsx b/x-pack/plugins/beats_management/public/components/inputs/password_input.tsx deleted file mode 100644 index 7d4b0c369c19ae..00000000000000 --- a/x-pack/plugins/beats_management/public/components/inputs/password_input.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CommonProps, EuiFieldPassword, EuiFieldPasswordProps, EuiFormRow } from '@elastic/eui'; -import { FormsyInputProps, withFormsy } from 'formsy-react'; -import React, { Component, InputHTMLAttributes } from 'react'; - -interface ComponentProps - extends FormsyInputProps, - CommonProps, - Omit { - instantValidation?: boolean; - label: string; - errorText: string; - fullWidth: boolean; - helpText: React.ReactElement; - compressed: boolean; - onChange?(e: React.ChangeEvent, value: any): void; - onBlur?(e: React.ChangeEvent, value: any): void; -} - -interface ComponentState { - allowError: boolean; -} - -class FieldPassword extends Component< - InputHTMLAttributes & ComponentProps, - ComponentState -> { - constructor(props: any) { - super(props); - - this.state = { - allowError: false, - }; - } - - public componentDidMount() { - const { defaultValue, setValue } = this.props; - if (defaultValue) { - setValue(defaultValue); - } - } - - public handleChange = (e: React.ChangeEvent) => { - const { value } = e.currentTarget; - this.props.setValue(value); - if (this.props.onChange) { - this.props.onChange(e, value); - } - if (this.props.instantValidation) { - this.showError(); - } - }; - - public handleBlur = (e: React.ChangeEvent) => { - this.showError(); - if (this.props.onBlur) { - this.props.onBlur(e, e.currentTarget.value); - } - }; - - public showError = () => this.setState({ allowError: true }); - - public render() { - const { - name, - id, - required, - label, - getValue, - isValid, - isPristine, - getErrorMessage, - fullWidth, - className, - disabled, - helpText, - onBlur, - } = this.props; - - const { allowError } = this.state; - const error = !isPristine() && !isValid() && allowError; - - return ( - - - - ); - } -} - -export const FormsyEuiPasswordText = withFormsy(FieldPassword); diff --git a/x-pack/plugins/beats_management/public/components/inputs/select.tsx b/x-pack/plugins/beats_management/public/components/inputs/select.tsx deleted file mode 100644 index edd1a7fee90c50..00000000000000 --- a/x-pack/plugins/beats_management/public/components/inputs/select.tsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiFormRow, - // @ts-ignore - EuiSelect, -} from '@elastic/eui'; -import { CommonProps } from '@elastic/eui/src/components/common'; -// @ts-ignore -import { FormsyInputProps, withFormsy } from 'formsy-react'; -import React, { Component, InputHTMLAttributes } from 'react'; - -const FixedSelect = EuiSelect as React.FC; - -interface ComponentProps extends FormsyInputProps, CommonProps { - instantValidation: boolean; - options: Array<{ value: string; text: string }>; - label: string; - errorText: string; - fullWidth: boolean; - helpText: React.ReactElement; - compressed: boolean; - onChange(e: React.ChangeEvent, value: any): void; - onBlur(e: React.ChangeEvent, value: any): void; -} - -interface ComponentState { - allowError: boolean; -} - -class FieldSelect extends Component< - InputHTMLAttributes & ComponentProps, - ComponentState -> { - public static defaultProps = { - passRequiredToField: true, - }; - - public state = { allowError: false }; - - public componentDidMount() { - const { defaultValue, setValue } = this.props; - if (defaultValue) { - setValue(defaultValue); - } - } - - public UNSAFE_componentWillReceiveProps(nextProps: ComponentProps) { - if (nextProps.isFormSubmitted()) { - this.showError(); - } - } - - public handleChange = (e: React.ChangeEvent) => { - const { value } = e.currentTarget; - - this.props.setValue(value); - if (this.props.onChange) { - this.props.onChange(e, e.currentTarget.value); - } - if (this.props.instantValidation) { - this.showError(); - } - }; - - public handleBlur = (e: React.ChangeEvent) => { - this.showError(); - if (this.props.onBlur) { - this.props.onBlur(e, e.currentTarget.value); - } - }; - - public showError = () => this.setState({ allowError: true }); - - public render() { - const { - id, - required, - label, - options, - getValue, - isValid, - isPristine, - getErrorMessage, - fullWidth, - className, - disabled, - helpText, - } = this.props; - - const { allowError } = this.state; - const error = !isPristine() && !isValid() && allowError; - - return ( - - - - ); - } -} - -export const FormsyEuiSelect = withFormsy(FieldSelect); diff --git a/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx b/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx deleted file mode 100644 index e61c3db2ebd139..00000000000000 --- a/x-pack/plugins/beats_management/public/components/layouts/no_data.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; -import React from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; - -interface LayoutProps extends RouteComponentProps { - children: React.ReactNode; - title: string | React.ReactNode; - actionSection?: React.ReactNode; -} - -export const NoDataLayout = withRouter(({ actionSection, title, children }: LayoutProps) => ( - - - - {title}} - body={children} - actions={actionSection} - /> - - - -)); diff --git a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx b/x-pack/plugins/beats_management/public/components/layouts/primary.tsx deleted file mode 100644 index 0a1c0bb028c77b..00000000000000 --- a/x-pack/plugins/beats_management/public/components/layouts/primary.tsx +++ /dev/null @@ -1,84 +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 { - EuiHeader, - EuiHeaderBreadcrumbs, - EuiHeaderSection, - EuiPage, - EuiPageBody, - EuiPageContent, - EuiPageContentBody, - EuiPageHeader, - EuiPageHeaderSection, - EuiTitle, -} from '@elastic/eui'; -import React, { Component, ReactNode } from 'react'; -import styled from 'styled-components'; -import { BreadcrumbConsumer } from '../navigation/breadcrumb'; - -type RenderCallback = (component: () => JSX.Element) => void; -interface PrimaryLayoutProps { - title: string | React.ReactNode; - actionSection?: React.ReactNode; - hideBreadcrumbs?: boolean; -} -export class PrimaryLayout extends Component { - private actionSection: (() => JSX.Element) | null = null; - constructor(props: PrimaryLayoutProps) { - super(props); - } - - public render() { - const children: (callback: RenderCallback) => void | ReactNode = this.props.children as any; - return ( - - {!this.props.hideBreadcrumbs && ( - - {({ breadcrumbs }) => ( - - - - - - )} - - )} - - - - - -

{this.props.title}

-
-
- - {(this.actionSection && this.actionSection()) || this.props.actionSection} - -
- - - {(children && typeof children === 'function' - ? children(this.renderAction) - : children) || } - - -
-
-
- ); - } - - private renderAction = (component: () => JSX.Element) => { - this.actionSection = component; - this.forceUpdate(); - }; -} - -const HeaderWrapper = styled(EuiHeader)` - height: 29px; -`; diff --git a/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx b/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx deleted file mode 100644 index 4d0fb9126d5b50..00000000000000 --- a/x-pack/plugins/beats_management/public/components/layouts/walkthrough.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { EuiPageContent, EuiPageContentBody, EuiStepsHorizontal, EuiTitle } from '@elastic/eui'; - -interface LayoutProps { - title: string; - goTo: (path: string) => any; - walkthroughSteps: Array<{ - id: string; - name: string; - }>; - activePath: string; -} - -export const WalkthroughLayout: React.FC = ({ - walkthroughSteps, - title, - activePath, - goTo, - children, -}) => { - const indexOfCurrent = walkthroughSteps.findIndex((step) => activePath === step.id); - return ( - - -

{title}

-
-
-
- ({ - title: step.name, - isComplete: i <= indexOfCurrent, - onClick: () => goTo(step.id), - }))} - /> -
-
- {children} -
- ); -}; diff --git a/x-pack/plugins/beats_management/public/components/loading.tsx b/x-pack/plugins/beats_management/public/components/loading.tsx deleted file mode 100644 index fabbee359946c8..00000000000000 --- a/x-pack/plugins/beats_management/public/components/loading.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import * as React from 'react'; - -export const Loading: React.FC<{}> = () => ( - - - - - -); diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx deleted file mode 100644 index 7af0270822422d..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/breadcrumb.tsx +++ /dev/null @@ -1,64 +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, { Component } from 'react'; -import { RouteProps } from 'react-router-dom'; -import { BASE_PATH } from '../../../../common/constants'; -import { BreadcrumbConsumer } from './consumer'; -import { Breadcrumb as BreadcrumbData, BreadcrumbContext } from './types'; - -interface BreadcrumbManagerProps extends RouteProps { - text: string; - href?: string; - parents?: BreadcrumbData[]; - context: BreadcrumbContext; -} - -class BreadcrumbManager extends Component { - public componentWillUnmount() { - const { text, href, context } = this.props; - - context.removeCrumb({ - text, - href, - }); - } - - public componentDidMount() { - const { text, href, parents, context } = this.props; - context.addCrumb( - { - text, - href, - }, - parents - ); - } - - public render() { - return ; - } -} - -interface BreadcrumbProps extends RouteProps { - title: string; - path?: string; - parentBreadcrumbs?: BreadcrumbData[]; -} - -export const Breadcrumb: React.FC = ({ title, path, parentBreadcrumbs }) => ( - - {(context) => ( - - )} - -); diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/consumer.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/consumer.tsx deleted file mode 100644 index 3062095a06383b..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/consumer.tsx +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { Consumer as BreadcrumbConsumer } from './context'; diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/context.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/context.tsx deleted file mode 100644 index 3d11194bc3fb9f..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/context.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { Breadcrumb, BreadcrumbContext } from './types'; - -/* istanbul ignore next */ -const defaultContext: BreadcrumbContext = { - breadcrumbs: [], - addCrumb: (crumb: Breadcrumb) => null, - removeCrumb: (crumb: Breadcrumb) => null, -}; - -export const { Provider, Consumer } = React.createContext(defaultContext); diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/index.ts b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/index.ts deleted file mode 100644 index e4839a75c39e06..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { BreadcrumbProvider } from './provider'; -export { BreadcrumbConsumer } from './consumer'; -export { Breadcrumb } from './breadcrumb'; diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx deleted file mode 100644 index 20919a52b658ed..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/provider.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Component, ReactElement } from 'react'; -import { Provider } from './context'; -import { Breadcrumb } from './types'; -import { services } from '../../../kbn_services'; - -interface ComponentProps { - useGlobalBreadcrumbs: boolean; - children: ReactElement | Array>; -} - -interface ComponentState { - breadcrumbs: Array<{ - href?: string; - breadcrumb: Breadcrumb; - parents?: Breadcrumb[]; - }>; -} - -export class BreadcrumbProvider extends Component { - public state = { - breadcrumbs: [] as ComponentState['breadcrumbs'], - }; - - public addCrumb = (breadcrumb: Breadcrumb, parents?: Breadcrumb[]) => { - this.setState(({ breadcrumbs: prevCrumbs }) => ({ - breadcrumbs: [ - ...prevCrumbs, - { - href: breadcrumb.href, - breadcrumb, - parents, - }, - ], - })); - }; - - public removeCrumb = (crumbToRemove: Breadcrumb) => { - this.setState(({ breadcrumbs: prevCrumbs }) => { - const breadcrumbs = prevCrumbs.filter((prevCrumb) => { - const { href } = prevCrumb; - return !(crumbToRemove.href === href); - }); - return { breadcrumbs }; - }); - }; - - public render() { - const { breadcrumbs } = this.state; - - const context = { - breadcrumbs: breadcrumbs.reduce((crumbs, crumbStorageItem) => { - if (crumbStorageItem.parents) { - crumbs = crumbs.concat(crumbStorageItem.parents); - } - crumbs.push(crumbStorageItem.breadcrumb); - return crumbs; - }, [] as Breadcrumb[]), - addCrumb: this.addCrumb, - removeCrumb: this.removeCrumb, - }; - if (this.props.useGlobalBreadcrumbs) { - services.setBreadcrumbs(context.breadcrumbs); - } - return {this.props.children}; - } -} diff --git a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/types.d.ts b/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/types.d.ts deleted file mode 100644 index 33a947d649c46e..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/breadcrumb/types.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface BreadcrumbContext { - breadcrumbs: Breadcrumb[]; - addCrumb: (crumb: Breadcrumb, parents?: Breadcrumb[]) => void; - removeCrumb: (crumb: Breadcrumb) => void; -} -export interface Breadcrumb { - text: string; - href?: string; -} diff --git a/x-pack/plugins/beats_management/public/components/navigation/child_routes.tsx b/x-pack/plugins/beats_management/public/components/navigation/child_routes.tsx deleted file mode 100644 index 9e4eaa98c41e95..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/child_routes.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { FC } from 'react'; -import { Route, Switch } from 'react-router-dom'; - -export interface RouteConfig { - path: string; - component: React.ComponentType; - routes?: RouteConfig[]; -} - -export const ChildRoutes: FC<{ - routes?: RouteConfig[]; - useSwitch?: boolean; - [other: string]: any; -}> = ({ routes, useSwitch = true, ...rest }) => { - if (!routes) { - return null; - } - const Parent = useSwitch ? Switch : React.Fragment; - return ( - - {routes.map((route) => ( - { - const Component = route.component; - return ; - }} - /> - ))} - - ); -}; diff --git a/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx b/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx deleted file mode 100644 index 3eb00c8714868d..00000000000000 --- a/x-pack/plugins/beats_management/public/components/navigation/connected_link.tsx +++ /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 { get } from 'lodash'; -import React from 'react'; - -import { EuiLink } from '@elastic/eui'; -import { Link, withRouter, RouteComponentProps } from 'react-router-dom'; - -interface ConnectedLinkComponent extends RouteComponentProps { - location: any; - path: string; - disabled: boolean; - query: any; - [key: string]: any; -} - -export const ConnectedLinkComponent = ({ - location, - path, - query, - disabled, - children, - ...props -}: ConnectedLinkComponent) => { - if (disabled) { - return ; - } - - // Shorthand for pathname - const pathname = path || get(props.to, 'pathname') || location.pathname; - - return ( - - ); -}; - -export const ConnectedLink = withRouter(ConnectedLinkComponent); diff --git a/x-pack/plugins/beats_management/public/components/table/action_schema.ts b/x-pack/plugins/beats_management/public/components/table/action_schema.ts deleted file mode 100644 index 254188e764590a..00000000000000 --- a/x-pack/plugins/beats_management/public/components/table/action_schema.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { AssignmentActionType } from './table'; - -export enum ActionComponentType { - Action, - Popover, - SelectionCount, - TagBadgeList, -} -export interface ControlSchema { - id?: number; - name: string; - danger?: boolean; - type: ActionComponentType; - action?: AssignmentActionType; - actionDataKey?: string; - showWarning?: boolean; - warningHeading?: string; - warningMessage?: string; - lazyLoad?: boolean; - grow?: boolean; -} - -export const beatsListActions: ControlSchema[] = [ - { - grow: false, - name: i18n.translate('xpack.beatsManagement.beatsListAssignmentOptions.unenrollButtonLabel', { - defaultMessage: 'Unenroll selected', - }), - showWarning: true, - type: ActionComponentType.Action, - warningHeading: i18n.translate( - 'xpack.beatsManagement.beatsListAssignmentOptions.unenrollBeatsWarninigTitle', - { defaultMessage: 'Unenroll selected beats?' } - ), - warningMessage: i18n.translate( - 'xpack.beatsManagement.beatsListAssignmentOptions.unenrollBeatsWarninigMessage', - { defaultMessage: 'The selected Beats will no longer use central management' } - ), - action: AssignmentActionType.Delete, - danger: true, - }, - { - name: i18n.translate('xpack.beatsManagement.beatsListAssignmentOptions.setTagsButtonLabel', { - defaultMessage: 'Set tags', - }), - grow: false, - type: ActionComponentType.TagBadgeList, - actionDataKey: 'tags', - lazyLoad: true, - }, -]; - -export const tagListActions: ControlSchema[] = [ - { - danger: true, - grow: false, - name: i18n.translate('xpack.beatsManagement.tagListAssignmentOptions.removeTagsButtonLabel', { - defaultMessage: 'Remove selected', - }), - type: ActionComponentType.Action, - showWarning: true, - warningHeading: i18n.translate( - 'xpack.beatsManagement.tagListAssignmentOptions.removeTagsWarninigTitle', - { defaultMessage: 'Remove tag(s)' } - ), - warningMessage: i18n.translate( - 'xpack.beatsManagement.tagListAssignmentOptions.removeTagWarninigMessage', - { defaultMessage: 'Remove the tag?' } - ), - action: AssignmentActionType.Delete, - }, -]; - -export const tagConfigActions: ControlSchema[] = [ - { - danger: true, - grow: false, - name: i18n.translate('xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsButtonLabel', { - defaultMessage: 'Remove tag(s)', - }), - type: ActionComponentType.Action, - showWarning: true, - warningHeading: i18n.translate( - 'xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsWarninigTitle', - { defaultMessage: 'Remove tag(s)' } - ), - warningMessage: i18n.translate( - 'xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsWarninigMessage', - { defaultMessage: 'Remove the tag from the selected beat(s)?' } - ), - action: AssignmentActionType.Delete, - }, -]; diff --git a/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx b/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx deleted file mode 100644 index 5badef9a71fe17..00000000000000 --- a/x-pack/plugins/beats_management/public/components/table/controls/action_control.tsx +++ /dev/null @@ -1,97 +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 { EuiButton, EuiConfirmModal } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import { AssignmentActionType } from '../table'; - -interface ActionControlProps { - action: AssignmentActionType; - disabled: boolean; - danger?: boolean; - name: string; - showWarning?: boolean; - warningHeading?: string; - warningMessage?: string; - actionHandler(action: AssignmentActionType, payload?: any): void; -} - -interface ActionControlState { - showModal: boolean; -} - -export class ActionControl extends React.PureComponent { - constructor(props: ActionControlProps) { - super(props); - - this.state = { - showModal: false, - }; - } - - public render() { - const { - action, - actionHandler, - danger, - name, - showWarning, - warningHeading, - warningMessage, - } = this.props; - - return ( -
- this.setState({ showModal: true }) : () => actionHandler(action) - } - > - {name} - - {this.state.showModal && ( - - } - confirmButtonText={ - - } - onConfirm={() => { - actionHandler(action); - this.setState({ showModal: false }); - }} - onCancel={() => this.setState({ showModal: false })} - title={ - warningHeading ? ( - warningHeading - ) : ( - - ) - } - > - {warningMessage} - - )} -
- ); - } -} diff --git a/x-pack/plugins/beats_management/public/components/table/controls/option_control.tsx b/x-pack/plugins/beats_management/public/components/table/controls/option_control.tsx deleted file mode 100644 index f07c84f584d918..00000000000000 --- a/x-pack/plugins/beats_management/public/components/table/controls/option_control.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ActionComponentType, ControlSchema } from '../action_schema'; -import { AssignmentActionType } from '../table'; -import { ActionControl } from './action_control'; -import { TagBadgeList } from './tag_badge_list'; - -interface ComponentProps extends ControlSchema { - actionData?: { - [key: string]: any; - }; - disabled: boolean; - actionHandler(action: AssignmentActionType, payload?: any): void; -} - -export const OptionControl: React.FC = (props: ComponentProps) => { - switch (props.type) { - case ActionComponentType.Action: - if (!props.action) { - throw Error('Action cannot be undefined'); - } - return ( - - ); - case ActionComponentType.TagBadgeList: - if (!props.actionDataKey) { - throw Error('actionDataKey cannot be undefined'); - } - if (!props.actionData) { - throw Error('actionData cannot be undefined'); - } - return ( - - ); - } - return
Invalid config
; -}; diff --git a/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx b/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx deleted file mode 100644 index 15cc3f3fce5771..00000000000000 --- a/x-pack/plugins/beats_management/public/components/table/controls/tag_badge_list.tsx +++ /dev/null @@ -1,110 +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 { - EuiButton, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPopover, -} from '@elastic/eui'; -import React from 'react'; -import { TABLE_CONFIG } from '../../../../common/constants/table'; -import { TagBadge } from '../../tag/tag_badge'; -import { AssignmentActionType } from '../index'; - -interface TagBadgeListProps { - items: object[]; - disabled: boolean; - name: string; - action?: AssignmentActionType; - actionHandler(action: AssignmentActionType, payload?: any): void; -} - -interface ComponentState { - isPopoverOpen: boolean; - items: object[]; -} - -export class TagBadgeList extends React.Component { - constructor(props: TagBadgeListProps) { - super(props); - - this.state = { - isPopoverOpen: false, - items: [], - }; - } - - public render() { - const button = ( - - {this.props.name} - - ); - - return ( - - - - {!this.props.items && } - {this.props.items && this.props.items.length === 0 && ( - - - No options avaliable - - - )} - {this.props.items && - this.props.items.map((tag: any) => ( - - - - - this.props.actionHandler(AssignmentActionType.Assign, tag.id) - } - onClickAriaLabel={tag.id} - tag={tag} - /> - - - - ))} - - - - ); - } - private onButtonClick = async () => { - this.props.actionHandler(AssignmentActionType.Reload); - this.setState((prevState) => ({ - isPopoverOpen: !prevState.isPopoverOpen, - })); - }; - - private closePopover = () => { - this.setState({ - isPopoverOpen: false, - }); - }; -} diff --git a/x-pack/plugins/beats_management/public/components/table/index.ts b/x-pack/plugins/beats_management/public/components/table/index.ts deleted file mode 100644 index bc6590c2e5c21c..00000000000000 --- a/x-pack/plugins/beats_management/public/components/table/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { beatsListActions, tagConfigActions } from './action_schema'; -export { AssignmentActionType, KueryBarProps, Table } from './table'; -export { - ActionDefinition, - BeatDetailTagsTable, - BeatsTableType, - FilterDefinition, - TagsTableType, -} from './table_type_configs'; diff --git a/x-pack/plugins/beats_management/public/components/table/table.tsx b/x-pack/plugins/beats_management/public/components/table/table.tsx deleted file mode 100644 index f22799c8b55b71..00000000000000 --- a/x-pack/plugins/beats_management/public/components/table/table.tsx +++ /dev/null @@ -1,160 +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 { EuiBasicTable, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import styled from 'styled-components'; -import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; -import { TABLE_CONFIG } from '../../../common/constants'; -import { AutocompleteField } from '../autocomplete_field/index'; -import { ControlSchema } from './action_schema'; -import { OptionControl } from './controls/option_control'; -import { TableType } from './table_type_configs'; - -export enum AssignmentActionType { - Add, - Assign, - Delete, - Edit, - Reload, - Search, -} - -export interface KueryBarProps { - filterQueryDraft: string; - isLoadingSuggestions: boolean; - isValid: boolean; - loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; - onChange?: (value: string) => void; - onSubmit?: (value: string) => void; - suggestions: QuerySuggestion[]; - value: string; -} - -interface TableProps { - actions?: ControlSchema[]; - actionData?: { - [key: string]: any; - }; - hideTableControls?: boolean; - kueryBarProps?: KueryBarProps; - items: any[]; - onTableChange?: (index: number, size: number) => void; - type: TableType; - actionHandler?(action: AssignmentActionType, payload?: any): void; -} - -interface TableState { - selection: any[]; - pageIndex: number; -} - -const TableContainer = styled.div` - padding: 16px; -`; - -export class Table extends React.Component { - constructor(props: any) { - super(props); - - this.state = { - selection: [], - pageIndex: 0, - }; - } - - public resetSelection = () => { - this.setSelection([]); - }; - - public setSelection = (selection: any[]) => { - this.setState({ - selection, - }); - }; - - public actionHandler = (action: AssignmentActionType, payload?: any): void => { - if (this.props.actionHandler) { - this.props.actionHandler(action, payload); - } - }; - - public render() { - const { actionData, actions, hideTableControls, items, kueryBarProps, type } = this.props; - - const pagination = { - pageIndex: this.state.pageIndex, - pageSize: TABLE_CONFIG.INITIAL_ROW_SIZE, - pageSizeOptions: TABLE_CONFIG.PAGE_SIZE_OPTIONS, - }; - - const selectionOptions = hideTableControls - ? undefined - : { - onSelectionChange: this.setSelection, - selectable: () => true, - selectableMessage: () => - i18n.translate('xpack.beatsManagement.table.selectThisBeatTooltip', { - defaultMessage: 'Select this beat', - }), - selection: this.state.selection, - }; - - return ( - - - {actions && - actions.map((action) => ( - - - - ))} - - {kueryBarProps && ( - - - - )} - - - - - - ); - } - - private onTableChange = ({ page }: { page: { index: number; size: number } }) => { - if (this.props.onTableChange) { - this.props.onTableChange(page.index, page.size); - } - this.setState({ - pageIndex: page.index, - }); - }; -} diff --git a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx b/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx deleted file mode 100644 index 56cf99b01e8d99..00000000000000 --- a/x-pack/plugins/beats_management/public/components/table/table_type_configs.tsx +++ /dev/null @@ -1,345 +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 { EuiFlexGroup, EuiFlexItem, EuiHealth, EuiToolTip, IconColor } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { sortBy, uniqBy } from 'lodash'; -import moment from 'moment'; -import React from 'react'; -import { BeatTag, CMBeat } from '../../../common/domain_types'; -import { ConnectedLink } from '../navigation/connected_link'; -import { TagBadge } from '../tag'; - -export interface ColumnDefinition { - align?: 'left' | 'right' | 'center' | undefined; - field: string; - name: string; - sortable?: boolean; - width?: string; - render?(value: any, object?: any): any; -} - -export interface ActionDefinition { - action: string; - danger?: boolean; - icon?: any; - name: string; -} - -interface FilterOption { - value: string; -} - -export interface FilterDefinition { - field: string; - name: string; - options?: FilterOption[]; - type: string; -} - -export interface ControlDefinitions { - actions: ActionDefinition[]; - filters: FilterDefinition[]; - primaryActions?: ActionDefinition[]; -} - -export interface TableType { - itemType: 'Beats' | 'Tags'; - columnDefinitions: ColumnDefinition[]; - controlDefinitions(items: any[]): ControlDefinitions; -} - -const dynamicStatuses = { - STARTING: { - color: 'success', - status: i18n.translate('xpack.beatsManagement.beatsTable.startingStatusLabel', { - defaultMessage: 'Starting', - }), - details: i18n.translate('xpack.beatsManagement.beatsTable.configStatus.startingTooltip', { - defaultMessage: 'This Beat is starting.', - }), - }, - IN_PROGRESS: { - color: 'warning', - status: i18n.translate('xpack.beatsManagement.beatsTable.updatingStatusLabel', { - defaultMessage: 'Updating', - }), - details: i18n.translate('xpack.beatsManagement.beatsTable.configStatus.progressTooltip', { - defaultMessage: 'This Beat is currently reloading config from CM.', - }), - }, - RUNNING: { - color: 'success', - status: i18n.translate('xpack.beatsManagement.beatsTable.runningStatusLabel', { - defaultMessage: 'Running', - }), - details: i18n.translate('xpack.beatsManagement.beatsTable.configStatus.runningTooltip', { - defaultMessage: 'This Beat is running without issues.', - }), - }, - CONFIG: { - color: 'danger', - status: i18n.translate('xpack.beatsManagement.beatsTable.configErrorStatusLabel', { - defaultMessage: 'Config error', - }), - }, - FAILED: { - color: 'danger', - status: i18n.translate('xpack.beatsManagement.beatsTable.failedStatusLabel', { - defaultMessage: 'Error', - }), - details: i18n.translate('xpack.beatsManagement.beatsTable.configStatus.errorTooltip', { - defaultMessage: 'There is an error on this beat, please check the logs for this host.', - }), - }, - STOPPED: { - color: 'danger', - status: i18n.translate('xpack.beatsManagement.beatsTable.stoppedStatusLabel', { - defaultMessage: 'stopped', - }), - details: i18n.translate('xpack.beatsManagement.beatsTable.configStatus.errorTooltip', { - defaultMessage: 'There is an error on this beat, please check the logs for this host.', - }), - }, -}; - -export const BeatsTableType: TableType = { - itemType: 'Beats', - columnDefinitions: [ - { - field: 'name', - name: i18n.translate('xpack.beatsManagement.beatsTable.beatNameTitle', { - defaultMessage: 'Beat name', - }), - render: (name: string, beat: CMBeat) => ( - {name} - ), - sortable: true, - }, - { - field: 'type', - name: i18n.translate('xpack.beatsManagement.beatsTable.typeTitle', { - defaultMessage: 'Type', - }), - sortable: true, - }, - { - field: 'full_tags', - name: i18n.translate('xpack.beatsManagement.beatsTable.tagsTitle', { - defaultMessage: 'Tags', - }), - render: (value: string, beat: CMBeat & { tags: BeatTag[] }) => ( - - {(sortBy(beat.tags, 'id') || []).map((tag) => ( - - - - - - ))} - - ), - sortable: false, - }, - { - field: 'config_status', - name: i18n.translate('xpack.beatsManagement.beatsTable.configStatusTitle', { - defaultMessage: 'Config Status', - }), - render: (value: string, beat: CMBeat) => { - let color: IconColor = 'success'; - let statusText = i18n.translate('xpack.beatsManagement.beatsTable.configStatus.okLabel', { - defaultMessage: 'OK', - }); - let tooltipText = i18n.translate( - 'xpack.beatsManagement.beatsTable.configStatus.okTooltip', - { - defaultMessage: 'Beat successfully applied latest config', - } - ); - - if (beat.status && moment().diff(beat.last_checkin, 'minutes') < 10) { - color = dynamicStatuses[beat.status.event.type].color; - statusText = dynamicStatuses[beat.status.event.type].status; - tooltipText = - (dynamicStatuses[beat.status.event.type] as any).details || beat.status.event.message; - } else if (!beat.status && moment().diff(beat.last_checkin, 'minutes') >= 10) { - color = 'danger'; - statusText = i18n.translate( - 'xpack.beatsManagement.beatsTable.configStatus.offlineLabel', - { - defaultMessage: 'Offline', - } - ); - tooltipText = i18n.translate( - 'xpack.beatsManagement.beatsTable.configStatus.noConnectionTooltip', - { - defaultMessage: 'This Beat has not connected to kibana in over 10min', - } - ); - } else if (beat.status && moment().diff(beat.last_checkin, 'minutes') >= 10) { - color = 'subdued'; - - tooltipText = i18n.translate( - 'xpack.beatsManagement.beatsTable.configStatus.notStartedTooltip', - { - defaultMessage: 'This Beat has not yet been started.', - } - ); - statusText = i18n.translate( - 'xpack.beatsManagement.beatsTable.configStatus.notStartedLabel', - { - defaultMessage: 'Not started', - } - ); - } else { - color = 'subdued'; - statusText = i18n.translate( - 'xpack.beatsManagement.beatsTable.configStatus.offlineLabel', - { - defaultMessage: 'Offline', - } - ); - } - - return ( - - - {statusText} - - - ); - }, - sortable: false, - }, - // { - // field: 'full_tags', - // name: i18n.translate('xpack.beatsManagement.beatsTable.lastConfigUpdateTitle', { - // defaultMessage: 'Last config update', - // }), - // render: (tags?: BeatTag[]) => - // tags && tags.length ? ( - // - // {moment(first(orderBy(tags, ['last_updated'], ['desc'])).last_updated).fromNow()} - // - // ) : null, - // sortable: true, - // }, - ], - controlDefinitions: (data: any[]) => ({ - actions: [ - { - name: i18n.translate('xpack.beatsManagement.beatsTable.disenrollSelectedLabel', { - defaultMessage: 'Unenroll Selected', - }), - action: 'delete', - danger: true, - }, - ], - filters: [ - { - type: 'field_value_selection', - field: 'type', - name: i18n.translate('xpack.beatsManagement.beatsTable.typeLabel', { - defaultMessage: 'Type', - }), - options: uniqBy( - data.map(({ type }: { type: any }) => ({ value: type })), - 'value' - ), - }, - ], - }), -}; - -export const TagsTableType: TableType = { - itemType: 'Tags', - columnDefinitions: [ - { - field: 'id', - name: i18n.translate('xpack.beatsManagement.tagsTable.tagNameTitle', { - defaultMessage: 'Tag name', - }), - render: (id: string, tag: BeatTag) => ( - - - - ), - sortable: true, - width: '45%', - }, - { - align: 'right', - field: 'last_updated', - name: i18n.translate('xpack.beatsManagement.tagsTable.lastUpdateTitle', { - defaultMessage: 'Last update', - }), - render: (lastUpdate: Date) =>
{moment(lastUpdate).fromNow()}
, - sortable: true, - }, - ], - controlDefinitions: (data: any) => ({ - actions: [ - { - name: i18n.translate('xpack.beatsManagement.tagsTable.removeSelectedLabel', { - defaultMessage: 'Remove Selected', - }), - action: 'delete', - danger: true, - }, - ], - filters: [], - }), -}; - -export const BeatDetailTagsTable: TableType = { - itemType: 'Tags', - columnDefinitions: [ - { - field: 'id', - name: i18n.translate('xpack.beatsManagement.beatTagsTable.tagNameTitle', { - defaultMessage: 'Tag name', - }), - render: (id: string, tag: BeatTag) => ( - - - - ), - sortable: true, - width: '55%', - }, - { - align: 'right', - field: 'last_updated', - name: i18n.translate('xpack.beatsManagement.beatTagsTable.lastUpdateTitle', { - defaultMessage: 'Last update', - }), - render: (lastUpdate: string) => {moment(lastUpdate).fromNow()}, - sortable: true, - }, - ], - controlDefinitions: (data: any) => ({ - actions: [], - filters: [], - primaryActions: [ - { - name: i18n.translate('xpack.beatsManagement.beatTagsTable.addTagLabel', { - defaultMessage: 'Add Tag', - }), - action: 'add', - danger: false, - }, - { - name: i18n.translate('xpack.beatsManagement.beatTagsTable.removeSelectedLabel', { - defaultMessage: 'Remove Selected', - }), - action: 'remove', - danger: true, - }, - ], - }), -}; diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx deleted file mode 100644 index a3512a395570f0..00000000000000 --- a/x-pack/plugins/beats_management/public/components/tag/config_view/config_form.tsx +++ /dev/null @@ -1,216 +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. - */ - -// @ts-ignore -import { i18n } from '@kbn/i18n'; -import Formsy from 'formsy-react'; -import { get } from 'lodash'; -import React from 'react'; -import { ConfigBlockSchema, ConfigurationBlock } from '../../../../common/domain_types'; -import { - FormsyEuiCodeEditor, - FormsyEuiFieldText, - FormsyEuiMultiFieldText, - FormsyEuiPasswordText, - FormsyEuiSelect, -} from '../../inputs'; - -interface ComponentProps { - values: ConfigurationBlock; - schema: ConfigBlockSchema; - id: string; - onSubmit?: (modal: any) => any; - canSubmit(canIt: boolean): any; -} - -interface ComponentState { - canSubmit: boolean; -} - -class ConfigFormUi extends React.Component { - private form = React.createRef(); - constructor(props: ComponentProps) { - super(props); - - this.state = { - canSubmit: false, - }; - } - - public enableButton = () => { - this.setState({ - canSubmit: true, - }); - this.props.canSubmit(true); - }; - public disableButton = () => { - this.setState({ - canSubmit: false, - }); - this.props.canSubmit(false); - }; - public submit = () => { - if (this.form.current && this.props.onSubmit) { - this.form.current.click(); - } - }; - public onValidSubmit = (model: ModelType) => { - if (!this.props.onSubmit) { - return; - } - - this.props.onSubmit(model); - }; - public render() { - return ( -
-
- - {this.props.schema.configs.map((schema) => { - switch (schema.ui.type) { - case 'input': - return ( - - ); - case 'password': - return ( - - ); - case 'multi-input': - return ( - - ); - case 'select': - return ( - - ); - case 'code': - return ( - - ); - } - })} - {this.props.schema && ( - - )} - {this.props.onSubmit && ( -
- ); - } -} -export const ConfigForm = ConfigFormUi; diff --git a/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx b/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx deleted file mode 100644 index 601d517e95e9e5..00000000000000 --- a/x-pack/plugins/beats_management/public/components/tag/config_view/index.tsx +++ /dev/null @@ -1,189 +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 { - EuiButton, - EuiButtonEmpty, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiFormRow, - EuiHorizontalRule, - EuiSelect, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { configBlockSchemas } from '../../../../common/config_schemas'; -import { translateConfigSchema } from '../../../../common/config_schemas_translations_map'; -import { ConfigurationBlock } from '../../../../common/domain_types'; -import { ConfigForm } from './config_form'; - -interface ComponentProps { - configBlock?: ConfigurationBlock; - onClose(): any; - onSave?(config: ConfigurationBlock): any; -} - -interface ComponentState { - valid: boolean; - configBlock: ConfigurationBlock; -} - -class ConfigViewUi extends React.Component { - private form = React.createRef(); - private editMode: boolean; - private schema = translateConfigSchema(configBlockSchemas); - constructor(props: any) { - super(props); - this.editMode = props.configBlock !== undefined; - - this.state = { - valid: false, - configBlock: props.configBlock || { - type: this.schema[0].id, - }, - }; - } - public onValueChange = (field: string) => (e: any) => { - const value = e.currentTarget ? e.currentTarget.value : e; - this.setState((state: any) => ({ - configBlock: { - ...state.configBlock, - [field]: value, - }, - })); - }; - public render() { - const thisConfigSchema = this.schema.find((s) => this.state.configBlock.type === s.id); - - if (!thisConfigSchema) { - return i18n.translate('xpack.beatsManagement.tagConfig.invalidSchema', { - defaultMessage: - 'Error: This config is invalid, it is not supported by Beats and should be removed', - }); - } - return ( - - - -

- {this.editMode - ? this.props.onSave - ? i18n.translate('xpack.beatsManagement.tagConfig.editConfigurationTitle', { - defaultMessage: 'Edit configuration block', - }) - : i18n.translate('xpack.beatsManagement.tagConfig.viewConfigurationTitle"', { - defaultMessage: 'View configuration block', - }) - : i18n.translate('xpack.beatsManagement.tagConfig.addConfigurationTitle"', { - defaultMessage: 'Add configuration block', - })} -

-
-
- - - ({ value: s.id, text: s.name }))} - value={this.state.configBlock.type} - disabled={this.editMode} - onChange={this.onValueChange('type')} - /> - - - - - -

- {i18n.translate('xpack.beatsManagement.tagConfig.configurationTypeText', { - defaultMessage: '{configType} configuration', - values: { - configType: thisConfigSchema ? thisConfigSchema.name : 'Unknown', - }, - })} -

- - - { - if (this.props.onSave) { - this.props.onSave({ - ...this.state.configBlock, - config: data, - }); - } - this.props.onClose(); - } - : undefined - } - canSubmit={(canIt) => this.setState({ valid: canIt })} - ref={this.form} - values={this.state.configBlock} - id={thisConfigSchema ? thisConfigSchema.name : 'Undefined'} - schema={thisConfigSchema} - /> -
- - - - - {i18n.translate('xpack.beatsManagement.tagConfig.closeButtonLabel', { - defaultMessage: 'Close', - })} - - - {this.props.onSave && ( - - { - if (this.form.current) { - this.form.current.submit(); - } - }} - > - {i18n.translate('xpack.beatsManagement.tagConfig.saveButtonLabel', { - defaultMessage: 'Save', - })} - - - )} - - -
- ); - } -} - -export const ConfigView = ConfigViewUi; diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx deleted file mode 100644 index 0940b9c4fbdf91..00000000000000 --- a/x-pack/plugins/beats_management/public/components/tag/tag_badge.tsx +++ /dev/null @@ -1,48 +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 { EuiBadge, EuiBadgeProps } from '@elastic/eui'; -import React from 'react'; -import { TABLE_CONFIG } from '../../../common/constants'; - -type TagBadgeProps = EuiBadgeProps & { - maxIdRenderSize?: number; - tag: { name: string; color: string; disabled?: boolean; id: string }; -}; - -export const TagBadge = (props: TagBadgeProps) => { - const { iconType, onClick, onClickAriaLabel, tag } = props; - const maxIdRenderSize = props.maxIdRenderSize || TABLE_CONFIG.TRUNCATE_TAG_LENGTH; - const idToRender = `${tag.name.substring(0, maxIdRenderSize)}${ - tag.name.length > maxIdRenderSize ? '...' : '' - }`; - - if (tag.disabled) { - return ( - - {idToRender} - - ); - } else if (onClick && onClickAriaLabel) { - return ( - } - onClickAriaLabel={onClickAriaLabel} - > - {idToRender} - - ); - } else { - return ( - - {idToRender} - - ); - } -}; diff --git a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx b/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx deleted file mode 100644 index bbf5a8407905e5..00000000000000 --- a/x-pack/plugins/beats_management/public/components/tag/tag_edit.tsx +++ /dev/null @@ -1,238 +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 { - EuiButton, - // @ts-ignore - EuiColorPicker, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - // @ts-ignore - EuiForm, - EuiFormRow, - EuiHorizontalRule, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import 'brace/mode/yaml'; -import 'brace/theme/github'; -import React from 'react'; -import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; -import { ConfigList } from '../config_list'; -import { AssignmentActionType, BeatsTableType, Table, tagConfigActions } from '../table'; -import { ConfigView } from './config_view'; -import { TagBadge } from './tag_badge'; - -interface TagEditProps { - tag: BeatTag; - configuration_blocks: { - error?: string | undefined; - list: ConfigurationBlock[]; - page: number; - total: number; - }; - onConfigListChange: (index: number, size: number) => void; - onDetachBeat?: (beatIds: string[]) => void; - onTagChange: (field: keyof BeatTag, value: string) => any; - onConfigAddOrEdit: (block: ConfigurationBlock) => any; - onConfigRemoved: (block: ConfigurationBlock) => any; - attachedBeats?: CMBeat[]; -} - -interface TagEditState { - showFlyout: boolean; - tableRef: any; - selectedConfig?: ConfigurationBlock; -} - -export class TagEdit extends React.PureComponent { - constructor(props: TagEditProps) { - super(props); - - this.state = { - showFlyout: false, - tableRef: React.createRef(), - }; - } - - public render() { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { tag, attachedBeats, configuration_blocks } = this.props; - - return ( -
- - - -

- -

-
- -

- -

-
-
- -
-
- - - - } - isInvalid={!!this.getNameError(tag.name)} - error={this.getNameError(tag.name) || undefined} - > - - - - - - - -
- - - - - -

- -

-
- -

- -

-
-
- -
- { - if (action === 'delete') { - this.props.onConfigRemoved(block); - } else { - this.setState({ - showFlyout: true, - selectedConfig: block, - }); - } - }} - /> -
- { - this.setState({ showFlyout: true }); - }} - > - - -
-
-
- - {attachedBeats && ( -
- - - -

- -

-
- - - )} - {this.state.showFlyout && ( - this.setState({ showFlyout: false, selectedConfig: undefined })} - onSave={(config: ConfigurationBlock) => { - this.setState({ showFlyout: false, selectedConfig: undefined }); - this.props.onConfigAddOrEdit(config); - }} - /> - )} - - ); - } - - private getNameError = (name: string) => { - if (name && name !== '' && name.search(/^[a-zA-Z0-9-]+$/) === -1) { - return i18n.translate('xpack.beatsManagement.tag.tagName.validationErrorMessage', { - defaultMessage: 'Tag name must consist of letters, numbers, and dashes only', - }); - } else { - return false; - } - }; - - private handleAssignmentActions = (action: AssignmentActionType) => { - switch (action) { - case AssignmentActionType.Delete: - const { selection } = this.state.tableRef.current.state; - if (this.props.onDetachBeat) { - this.props.onDetachBeat(selection.map((beat: any) => beat.id)); - } - } - }; - - private updateTag = (key: keyof BeatTag, value?: any) => - value !== undefined - ? this.props.onTagChange(key, value) - : (e: any) => this.props.onTagChange(key, e.target ? e.target.value : e); -} diff --git a/x-pack/plugins/beats_management/public/containers/beats.ts b/x-pack/plugins/beats_management/public/containers/beats.ts deleted file mode 100644 index 73f77c08282e10..00000000000000 --- a/x-pack/plugins/beats_management/public/containers/beats.ts +++ /dev/null @@ -1,104 +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 { Container } from 'unstated'; -import { CMBeat } from '../../common/domain_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { BeatsTagAssignment } from '../../server/lib/adapters/beats/adapter_types'; -import { FrontendLibs } from './../lib/types'; - -interface ContainerState { - list: CMBeat[]; -} - -export class BeatsContainer extends Container { - private query?: string; - constructor(private readonly libs: FrontendLibs) { - super(); - this.state = { - list: [], - }; - } - - public getBeatWithToken = async (token: string) => { - const beat = await this.libs.beats.getBeatWithToken(token); - - if (beat) { - this.setState({ - list: [beat as CMBeat, ...this.state.list], - }); - return beat as CMBeat; - } - return null; - }; - - public reload = async (kuery?: string) => { - if (kuery) { - this.query = kuery; - } else { - this.query = undefined; - } - const beats = await this.libs.beats.getAll(this.query); - - this.setState({ - list: beats, - }); - }; - - public deactivate = async (beats: CMBeat[]) => { - for (const beat of beats) { - await this.libs.beats.update(beat.id, { active: false }); - } - - // because the compile code above has a very minor race condition, we wait, - // the max race condition time is really 10ms but doing 100 to be safe - setTimeout(async () => { - await this.reload(this.query); - }, 100); - }; - - public toggleTagAssignment = async (tagId: string, beats: CMBeat[]) => { - if (beats.some((beat) => beat.tags !== undefined && beat.tags.some((id) => id === tagId))) { - await this.removeTagsFromBeats(beats, tagId); - return 'removed'; - } - await this.assignTagsToBeats(beats, tagId); - return 'added'; - }; - - public removeTagsFromBeats = async (beats: CMBeat[] | string[], tagId: string) => { - if (!beats.length) { - return false; - } - const assignments = createBeatTagAssignments(beats, tagId); - await this.libs.beats.removeTagsFromBeats(assignments); - // ES responds incorrectly when we call too soon - setTimeout(async () => { - await this.reload(this.query); - }, 150); - }; - - public assignTagsToBeats = async (beats: CMBeat[] | string[], tagId: string) => { - if (!beats.length) { - return false; - } - const assignments = createBeatTagAssignments(beats, tagId); - await this.libs.beats.assignTagsToBeats(assignments); - // ES responds incorrectly when we call too soon - setTimeout(async () => { - await this.reload(this.query); - }, 150); - }; -} - -function createBeatTagAssignments(beats: CMBeat[] | string[], tagId: string): BeatsTagAssignment[] { - if (typeof beats[0] === 'string') { - return (beats as string[]).map((id) => ({ beatId: id, tag: tagId })); - } else { - return (beats as CMBeat[]).map(({ id }) => ({ beatId: id, tag: tagId })); - } -} diff --git a/x-pack/plugins/beats_management/public/containers/tags.ts b/x-pack/plugins/beats_management/public/containers/tags.ts deleted file mode 100644 index f7b4996fc750d8..00000000000000 --- a/x-pack/plugins/beats_management/public/containers/tags.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Container } from 'unstated'; -import { BeatTag } from '../../common/domain_types'; -import { FrontendLibs } from '../lib/types'; - -interface ContainerState { - list: BeatTag[]; -} - -export class TagsContainer extends Container { - private query?: string; - constructor(private readonly libs: FrontendLibs) { - super(); - this.state = { - list: [], - }; - } - public reload = async (kuery?: string) => { - if (kuery) { - this.query = kuery; - } else { - this.query = undefined; - } - - const tags = await this.libs.tags.getAll(this.query); - - this.setState({ - list: tags, - }); - }; - - public delete = async (tags: BeatTag[]) => { - const tagIds = tags.map((tag: BeatTag) => tag.id); - const success = await this.libs.tags.delete(tagIds); - if (success) { - this.setState({ - list: this.state.list.filter((tag) => tagIds.includes(tag.id)), - }); - } - return success; - }; - - public upsertTag = async (tag: BeatTag) => { - const beatTag = await this.libs.tags.upsertTag(tag); - await this.reload(); - return beatTag !== null; - }; -} diff --git a/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx deleted file mode 100644 index 5bd3a4194d4f77..00000000000000 --- a/x-pack/plugins/beats_management/public/containers/with_kuery_autocompletion.tsx +++ /dev/null @@ -1,89 +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 { QuerySuggestion } from '../../../../../src/plugins/data/public'; - -import { FrontendLibs } from '../lib/types'; -import { RendererFunction } from '../utils/typed_react'; - -interface WithKueryAutocompletionLifecycleProps { - libs: FrontendLibs; - fieldPrefix?: string; - children: RendererFunction<{ - isLoadingSuggestions: boolean; - loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: QuerySuggestion[]; - }>; -} - -interface WithKueryAutocompletionLifecycleState { - // lacking cancellation support in the autocompletion api, - // this is used to keep older, slower requests from clobbering newer ones - currentRequest: { - expression: string; - cursorPosition: number; - } | null; - suggestions: QuerySuggestion[]; -} - -export class WithKueryAutocompletion extends React.Component< - WithKueryAutocompletionLifecycleProps, - WithKueryAutocompletionLifecycleState -> { - public readonly state: WithKueryAutocompletionLifecycleState = { - currentRequest: null, - suggestions: [], - }; - - public render() { - const { currentRequest, suggestions } = this.state; - - return this.props.children({ - isLoadingSuggestions: currentRequest !== null, - loadSuggestions: this.loadSuggestions, - suggestions, - }); - } - - private loadSuggestions = async ( - expression: string, - cursorPosition: number, - maxSuggestions?: number - ) => { - this.setState({ - currentRequest: { - expression, - cursorPosition, - }, - suggestions: [], - }); - let suggestions: any[] = []; - try { - suggestions = await this.props.libs.elasticsearch.getSuggestions( - expression, - cursorPosition, - this.props.fieldPrefix - ); - } catch (e) { - suggestions = []; - } - - this.setState((state) => - state.currentRequest && - state.currentRequest.expression !== expression && - state.currentRequest.cursorPosition !== cursorPosition - ? state // ignore this result, since a newer request is in flight - : { - ...state, - currentRequest: null, - suggestions: maxSuggestions ? suggestions.slice(0, maxSuggestions) : suggestions, - } - ); - }; -} diff --git a/x-pack/plugins/beats_management/public/containers/with_url_state.tsx b/x-pack/plugins/beats_management/public/containers/with_url_state.tsx deleted file mode 100644 index a72a60bf04a184..00000000000000 --- a/x-pack/plugins/beats_management/public/containers/with_url_state.tsx +++ /dev/null @@ -1,101 +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 { parse, stringify } from 'query-string'; -import React from 'react'; -import { withRouter, RouteComponentProps } from 'react-router-dom'; -import { FlatObject } from '../frontend_types'; -import { RendererFunction } from '../utils/typed_react'; - -type StateCallback = (previousState: T) => T; - -export interface URLStateProps { - goTo: (path: string) => void; - setUrlState: ( - newState: - | Partial> - | StateCallback - | Promise> - ) => void; - urlState: URLState; -} -interface ComponentProps extends RouteComponentProps { - children: RendererFunction>; -} - -export class WithURLStateComponent extends React.Component< - ComponentProps -> { - private get URLState(): URLState { - // slice because parse does not account for the initial ? in the search string - return parse(decodeURIComponent(this.props.history.location.search).substring(1), { - sort: false, - }) as URLState; - } - - private historyListener: (() => void) | null = null; - - public componentWillUnmount() { - if (this.historyListener) { - this.historyListener(); - } - } - public render() { - return this.props.children({ - goTo: this.goTo, - setUrlState: this.setURLState, - urlState: this.URLState || {}, - }); - } - - private setURLState = async ( - state: - | Partial> - | StateCallback - | Promise> - ) => { - let newState; - const pastState = this.URLState; - if (typeof state === 'function') { - newState = await state(pastState); - } else { - newState = state; - } - - const search: string = stringify( - { - ...pastState, - ...newState, - }, - { sort: false } - ); - - const newLocation = { - ...this.props.history.location, - search, - }; - - this.props.history.replace(newLocation); - this.forceUpdate(); - }; - - private goTo = (path: string) => { - this.props.history.push({ - pathname: path, - search: this.props.history.location.search, - }); - }; -} -export const WithURLState = withRouter(WithURLStateComponent); - -export function withUrlState(UnwrappedComponent: React.ComponentType) { - return (origProps: OP) => ( - - {(URLProps: URLStateProps) => } - - ); -} diff --git a/x-pack/plugins/beats_management/public/frontend_types.d.ts b/x-pack/plugins/beats_management/public/frontend_types.d.ts deleted file mode 100644 index 2fa6da067d2b0d..00000000000000 --- a/x-pack/plugins/beats_management/public/frontend_types.d.ts +++ /dev/null @@ -1,36 +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 { RouteComponentProps } from 'react-router-dom'; -import { BeatsContainer } from './containers/beats'; -import { TagsContainer } from './containers/tags'; -import { URLStateProps } from './containers/with_url_state'; -import { FrontendLibs } from './lib/types'; - -export type FlatObject = { [Key in keyof T]: string }; - -export interface AppURLState { - beatsKBar?: string; - tagsKBar?: string; - enrollmentToken?: string; - createdTag?: string; -} - -export interface RouteConfig { - path: string; - component: React.ComponentType; - routes?: RouteConfig[]; -} - -export interface AppPageProps extends URLStateProps, RouteComponentProps { - libs: FrontendLibs; - containers: { - beats: BeatsContainer; - tags: TagsContainer; - }; - routes?: RouteConfig[]; -} diff --git a/x-pack/plugins/beats_management/public/index.ts b/x-pack/plugins/beats_management/public/index.ts deleted file mode 100644 index da124a3c50da13..00000000000000 --- a/x-pack/plugins/beats_management/public/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CoreSetup, Plugin, PluginInitializerContext } from '../../../../src/core/public'; - -import { ManagementSetup } from '../../../../src/plugins/management/public'; -import { SecurityPluginSetup } from '../../security/public'; -import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { LicensingPluginSetup } from '../../licensing/public'; - -import { bootstrap } from './bootstrap'; -import { BeatsManagementConfigType } from '../common'; - -interface SetupDeps { - management: ManagementSetup; - licensing: LicensingPluginSetup; - security?: SecurityPluginSetup; -} - -interface StartDeps { - data: DataPublicPluginStart; -} - -class BeatsManagementPlugin implements Plugin { - constructor(private readonly initContext: PluginInitializerContext) {} - - public setup(core: CoreSetup, plugins: SetupDeps) { - const config = this.initContext.config.get(); - bootstrap(core, plugins, config, this.initContext.env.packageInfo.version); - } - - public start() {} - public stop() {} -} - -export const plugin = (init: PluginInitializerContext) => new BeatsManagementPlugin(init); diff --git a/x-pack/plugins/beats_management/public/kbn_services.ts b/x-pack/plugins/beats_management/public/kbn_services.ts deleted file mode 100644 index 7c7ddef0dabc4e..00000000000000 --- a/x-pack/plugins/beats_management/public/kbn_services.ts +++ /dev/null @@ -1,26 +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 { CoreStart } from '../../../../src/core/public'; -import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; -import { ManagementAppMountParams } from '../../../../src/plugins/management/public'; - -export const services = { - I18nContext: (null as any) as CoreStart['i18n']['Context'], - setBreadcrumbs: (null as any) as ManagementAppMountParams['setBreadcrumbs'], - dataStart: (null as any) as DataPublicPluginStart, -}; - -export const setServices = ( - core: CoreStart, - plugins: { data: DataPublicPluginStart }, - params: ManagementAppMountParams -) => { - services.I18nContext = core.i18n.Context; - services.setBreadcrumbs = params.setBreadcrumbs; - services.dataStart = plugins.data; -}; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts deleted file mode 100644 index da61655e2c8564..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/adapter_types.ts +++ /dev/null @@ -1,38 +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 { CMBeat } from '../../../../common/domain_types'; -import { ReturnTypeBulkAction } from '../../../../common/return_types'; - -export interface CMBeatsAdapter { - get(id: string): Promise; - update(id: string, beatData: Partial): Promise; - getBeatsWithTag(tagId: string): Promise; - getAll(ESQuery?: any): Promise; - removeTagsFromBeats(removals: BeatsTagAssignment[]): Promise; - assignTagsToBeats(assignments: BeatsTagAssignment[]): Promise; - getBeatWithToken(enrollmentToken: string): Promise; -} - -export interface BeatsTagAssignment { - beatId: string; - tag: string; - idxInRequest?: number; -} - -interface BeatsReturnedTagAssignment { - status: number | null; - result?: string; -} - -export interface CMAssignmentReturn { - assignments: BeatsReturnedTagAssignment[]; -} - -export interface BeatsRemovalReturn { - removals: BeatsReturnedTagAssignment[]; -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts deleted file mode 100644 index 172635fb14f9ad..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/memory_beats_adapter.ts +++ /dev/null @@ -1,109 +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 { omit } from 'lodash'; -import { CMBeat } from '../../../../common/domain_types'; -import { ReturnTypeBulkAction } from '../../../../common/return_types'; -import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; - -export class MemoryBeatsAdapter implements CMBeatsAdapter { - private beatsDB: CMBeat[]; - - constructor(beatsDB: CMBeat[]) { - this.beatsDB = beatsDB; - } - - public async get(id: string) { - return this.beatsDB.find((beat) => beat.id === id) || null; - } - - public async update(id: string, beatData: Partial): Promise { - const index = this.beatsDB.findIndex((beat) => beat.id === id); - - if (index === -1) { - return false; - } - - this.beatsDB[index] = { ...this.beatsDB[index], ...beatData }; - return true; - } - - public async getAll() { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])) as CMBeat[]; - } - public async getBeatsWithTag(tagId: string): Promise { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token'])) as CMBeat[]; - } - - public async getBeatWithToken(enrollmentToken: string): Promise { - return this.beatsDB.map((beat: any) => omit(beat, ['access_token']))[0] as CMBeat | null; - } - public async removeTagsFromBeats( - removals: BeatsTagAssignment[] - ): Promise { - const beatIds = removals.map((r) => r.beatId); - - const response = this.beatsDB - .filter((beat) => beatIds.includes(beat.id)) - .map((beat) => { - const tagData = removals.find((r) => r.beatId === beat.id); - if (tagData) { - if (beat.tags) { - beat.tags = beat.tags.filter((tag) => tag !== tagData.tag); - } - } - const removalsForBeat = removals.filter((r) => r.beatId === beat.id); - if (removalsForBeat.length) { - removalsForBeat.forEach((assignment: BeatsTagAssignment) => { - if (beat.tags) { - beat.tags = beat.tags.filter((tag) => tag !== assignment.tag); - } - }); - } - return beat; - }); - - return response.map((item: CMBeat, resultIdx: number) => ({ - idxInRequest: removals[resultIdx].idxInRequest, - result: 'updated', - status: 200, - })) as any; - } - - public async assignTagsToBeats( - assignments: BeatsTagAssignment[] - ): Promise { - const beatIds = assignments.map((r) => r.beatId); - - this.beatsDB - .filter((beat) => beatIds.includes(beat.id)) - .map((beat) => { - // get tags that need to be assigned to this beat - const tags = assignments - .filter((a) => a.beatId === beat.id) - .map((t: BeatsTagAssignment) => t.tag); - - if (tags.length > 0) { - if (!beat.tags) { - beat.tags = []; - } - const nonExistingTags = tags.filter((t: string) => beat.tags && !beat.tags.includes(t)); - - if (nonExistingTags.length > 0) { - beat.tags = beat.tags.concat(nonExistingTags); - } - } - return beat; - }); - - return assignments.map((item: BeatsTagAssignment, resultIdx: number) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: 'updated', - status: 200, - })); - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts deleted file mode 100644 index 489eb4d721cd34..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/beats/rest_beats_adapter.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CMBeat } from '../../../../common/domain_types'; -import { - ReturnTypeBulkAction, - ReturnTypeGet, - ReturnTypeList, - ReturnTypeUpdate, -} from '../../../../common/return_types'; -import { RestAPIAdapter } from '../rest_api/adapter_types'; -import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; - -export class RestBeatsAdapter implements CMBeatsAdapter { - constructor(private readonly REST: RestAPIAdapter) {} - - public async get(id: string): Promise { - try { - return (await this.REST.get>(`/api/beats/agent/${id}`)).item; - } catch (e) { - return null; - } - } - - public async getBeatWithToken(enrollmentToken: string): Promise { - try { - return ( - await this.REST.get>(`/api/beats/agent/unknown/${enrollmentToken}`) - ).item; - } catch (e) { - return null; - } - } - - public async getAll(ESQuery?: string): Promise { - try { - return (await this.REST.get>('/api/beats/agents/all', { ESQuery })) - .list; - } catch (e) { - return []; - } - } - - public async getBeatsWithTag(tagId: string): Promise { - try { - return (await this.REST.get>(`/api/beats/agents/tag/${tagId}`)).list; - } catch (e) { - return []; - } - } - - public async update(id: string, beatData: Partial): Promise { - await this.REST.put>(`/api/beats/agent/${id}`, beatData); - return true; - } - - public async removeTagsFromBeats( - removals: BeatsTagAssignment[] - ): Promise { - return ( - await this.REST.post(`/api/beats/agents_tags/removals`, { - removals, - }) - ).results; - } - - public async assignTagsToBeats( - assignments: BeatsTagAssignment[] - ): Promise { - return ( - await this.REST.post(`/api/beats/agents_tags/assignments`, { - assignments, - }) - ).results; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/adapter_types.ts deleted file mode 100644 index 24921f1778b6c0..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/adapter_types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ConfigurationBlock } from '../../../../common/domain_types'; -import { ReturnTypeBulkUpsert, ReturnTypeList } from '../../../../common/return_types'; - -export interface FrontendConfigBlocksAdapter { - upsert(blocks: ConfigurationBlock[]): Promise; - getForTags(tagIds: string[], page: number): Promise>; - delete(id: string): Promise; -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/memory_config_blocks_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/memory_config_blocks_adapter.ts deleted file mode 100644 index 6b3549465e171f..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/memory_config_blocks_adapter.ts +++ /dev/null @@ -1,42 +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 { ConfigurationBlock } from '../../../../common/domain_types'; -import { ReturnTypeBulkUpsert, ReturnTypeList } from '../../../../common/return_types'; -import { FrontendConfigBlocksAdapter } from './adapter_types'; - -export class MemoryConfigBlocksAdapter implements FrontendConfigBlocksAdapter { - constructor(private db: ConfigurationBlock[]) {} - - public async upsert(blocks: ConfigurationBlock[]): Promise { - this.db = this.db.concat(blocks); - return { - success: true, - results: blocks.map(() => ({ - success: true, - action: 'created', - })), - } as ReturnTypeBulkUpsert; - } - public async getForTags(tagIds: string[]): Promise> { - return { - success: true, - list: this.db.filter((block) => tagIds.includes(block.tag)), - page: 0, - total: this.db.filter((block) => tagIds.includes(block.tag)).length, - }; - } - public async delete(id: string): Promise { - this.db = this.db.reduce((newDB: ConfigurationBlock[], block) => { - if (block.id !== id) { - newDB.push(block); - } - return newDB; - }, []); - return !!this.db.find((block) => block.id === id); - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/rest_config_blocks_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/rest_config_blocks_adapter.ts deleted file mode 100644 index 295455dfaa48d1..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/configuration_blocks/rest_config_blocks_adapter.ts +++ /dev/null @@ -1,36 +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 { ConfigurationBlock } from '../../../../common/domain_types'; -import { - ReturnTypeBulkDelete, - ReturnTypeBulkUpsert, - ReturnTypeList, -} from '../../../../common/return_types'; -import { RestAPIAdapter } from '../rest_api/adapter_types'; -import { FrontendConfigBlocksAdapter } from './adapter_types'; - -export class RestConfigBlocksAdapter implements FrontendConfigBlocksAdapter { - constructor(private readonly REST: RestAPIAdapter) {} - - public async upsert(blocks: ConfigurationBlock[]) { - const result = await this.REST.put(`/api/beats/configurations`, blocks); - return result; - } - public async getForTags( - tagIds: string[], - page: number - ): Promise> { - return await this.REST.get>( - `/api/beats/configurations/${tagIds.join(',')}/${page}` - ); - } - public async delete(id: string): Promise { - return (await this.REST.delete(`/api/beats/configurations/${id}`)) - .success; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts deleted file mode 100644 index b414648a192cb3..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/adapter_types.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; - -export interface ElasticsearchAdapter { - convertKueryToEsQuery: (kuery: string) => Promise; - getSuggestions: (kuery: string, selectionStart: any) => Promise; - isKueryValid(kuery: string): boolean; -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts deleted file mode 100644 index e5a24f53d049eb..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/memory.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; -import { ElasticsearchAdapter } from './adapter_types'; - -export class MemoryElasticsearchAdapter implements ElasticsearchAdapter { - constructor( - private readonly mockIsKueryValid: (kuery: string) => boolean, - private readonly mockKueryToEsQuery: (kuery: string) => string, - private readonly suggestions: QuerySuggestion[] - ) {} - - public isKueryValid(kuery: string): boolean { - return this.mockIsKueryValid(kuery); - } - public async convertKueryToEsQuery(kuery: string): Promise { - return this.mockKueryToEsQuery(kuery); - } - public async getSuggestions(kuery: string, selectionStart: any): Promise { - return this.suggestions; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts b/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts deleted file mode 100644 index 0143fc8b4a364a..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/elasticsearch/rest.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isEmpty } from 'lodash'; -import { ElasticsearchAdapter } from './adapter_types'; -import { QuerySuggestion, esKuery } from '../../../../../../../src/plugins/data/public'; -import { services } from '../../../kbn_services'; - -export class RestElasticsearchAdapter implements ElasticsearchAdapter { - private cachedIndexPattern: any = null; - constructor(private readonly indexPatternName: string) {} - - public isKueryValid(kuery: string): boolean { - try { - esKuery.fromKueryExpression(kuery); - } catch (err) { - return false; - } - - return true; - } - public async convertKueryToEsQuery(kuery: string): Promise { - if (!this.isKueryValid(kuery)) { - return ''; - } - const ast = esKuery.fromKueryExpression(kuery); - const indexPattern = await this.getIndexPattern(); - return JSON.stringify(esKuery.toElasticsearchQuery(ast, indexPattern)); - } - - public async getSuggestions(kuery: string, selectionStart: any): Promise { - const indexPattern = await this.getIndexPattern(); - - return ( - (await services.dataStart.autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - boolFilter: [], - query: kuery || '', - selectionStart, - selectionEnd: selectionStart, - })) || [] - ); - } - - private async getIndexPattern() { - if (this.cachedIndexPattern) { - return this.cachedIndexPattern; - } - const res = await services.dataStart.indexPatterns.getFieldsForWildcard({ - pattern: this.indexPatternName, - }); - if (isEmpty(res.fields)) { - return; - } - this.cachedIndexPattern = { - fields: res.fields, - title: `${this.indexPatternName}`, - }; - return this.cachedIndexPattern; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts deleted file mode 100644 index c9a0e6029dadae..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/adapter_types.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as t from 'io-ts'; -import { LICENSES } from '../../../../common/constants/security'; -import { RegisterManagementAppArgs } from '../../../../../../../src/plugins/management/public'; - -export interface FrameworkAdapter { - // Instance vars - info: FrameworkInfo; - version: string; - currentUser: FrameworkUser; - // Methods - waitUntilFrameworkReady(): Promise; - registerManagementUI(mount: RegisterManagementAppArgs['mount']): void; -} - -export const RuntimeFrameworkInfo = t.type({ - basePath: t.string, - license: t.type({ - type: t.keyof(Object.fromEntries(LICENSES.map((s) => [s, null])) as Record), - expired: t.boolean, - expiry_date_in_millis: t.number, - }), - security: t.type({ - enabled: t.boolean, - available: t.boolean, - }), - settings: t.type({ - encryptionKey: t.string, - enrollmentTokensTtlInSeconds: t.number, - defaultUserRoles: t.array(t.string), - }), -}); - -export interface FrameworkInfo extends t.TypeOf {} - -export const RuntimeFrameworkUser = t.interface( - { - username: t.string, - roles: t.readonlyArray(t.string), - full_name: t.union([t.null, t.string]), - email: t.union([t.null, t.string]), - enabled: t.boolean, - }, - 'FrameworkUser' -); -export interface FrameworkUser extends t.TypeOf {} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts deleted file mode 100644 index 03a9a776084984..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/kibana_framework_adapter.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable max-classes-per-file */ -import { IScope } from 'angular'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { first } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { MANAGEMENT_SECTION } from '../../../../common/constants'; -import { SecurityPluginSetup } from '../../../../../security/public'; -import { BufferedKibanaServiceCall, KibanaAdapterServiceRefs, KibanaUIConfig } from '../../types'; -import { - FrameworkAdapter, - FrameworkInfo, - FrameworkUser, - RuntimeFrameworkInfo, - RuntimeFrameworkUser, -} from './adapter_types'; -import { - ManagementSetup, - RegisterManagementAppArgs, -} from '../../../../../../../src/plugins/management/public'; -import { LicensingPluginSetup } from '../../../../../licensing/public'; -import { BeatsManagementConfigType } from '../../../../common'; - -export class KibanaFrameworkAdapter implements FrameworkAdapter { - public get info() { - if (this.xpackInfo) { - return this.xpackInfo; - } else { - throw new Error('framework adapter must have init called before anything else'); - } - } - - public get currentUser() { - return this.shieldUser!; - } - private xpackInfo: FrameworkInfo | null = null; - private adapterService: KibanaAdapterServiceProvider; - private shieldUser: FrameworkUser | null = null; - constructor( - private readonly PLUGIN_ID: string, - private readonly management: ManagementSetup, - private readonly getBasePath: () => string, - private readonly licensing: LicensingPluginSetup, - private readonly securitySetup: SecurityPluginSetup | undefined, - private readonly config: BeatsManagementConfigType, - public readonly version: string - ) { - this.adapterService = new KibanaAdapterServiceProvider(); - } - - public setUISettings = (key: string, value: any) => { - this.adapterService.callOrBuffer(({ config }) => { - config.set(key, value); - }); - }; - - public async waitUntilFrameworkReady(): Promise { - const license = await this.licensing.license$.pipe(first()).toPromise(); - let xpackInfoUnpacked: FrameworkInfo; - - try { - xpackInfoUnpacked = { - basePath: this.getBasePath(), - license: { - type: license.type ?? 'oss', - expired: !license.isActive, - expiry_date_in_millis: license.expiryDateInMillis ?? -1, - }, - security: { - enabled: license.getFeature('security').isEnabled, - available: license.getFeature('security').isAvailable, - }, - settings: this.config, - }; - } catch (e) { - throw new Error(`Unexpected data structure from xpackInfoService, ${JSON.stringify(e)}`); - } - - const assertData = RuntimeFrameworkInfo.decode(xpackInfoUnpacked); - if (isLeft(assertData)) { - throw new Error( - `Error parsing xpack info in ${this.PLUGIN_ID}, ${PathReporter.report(assertData)[0]}` - ); - } - this.xpackInfo = xpackInfoUnpacked; - - try { - this.shieldUser = (await this.securitySetup?.authc.getCurrentUser()) || null; - const assertUser = RuntimeFrameworkUser.decode(this.shieldUser); - - if (isLeft(assertUser)) { - throw new Error( - `Error parsing user info in ${this.PLUGIN_ID}, ${PathReporter.report(assertUser)[0]}` - ); - } - } catch (e) { - this.shieldUser = null; - } - } - - public registerManagementUI(mount: RegisterManagementAppArgs['mount']) { - const section = this.management.sections.section.ingest; - section.registerApp({ - id: MANAGEMENT_SECTION, - title: i18n.translate('xpack.beatsManagement.centralManagementLinkLabel', { - defaultMessage: 'Beats Central Management', - }), - order: 2, - mount, - }); - } -} - -class KibanaAdapterServiceProvider { - public serviceRefs: KibanaAdapterServiceRefs | null = null; - public bufferedCalls: Array> = []; - - public $get($rootScope: IScope, config: KibanaUIConfig) { - this.serviceRefs = { - config, - rootScope: $rootScope, - }; - - this.applyBufferedCalls(this.bufferedCalls); - - return this; - } - - public callOrBuffer(serviceCall: (serviceRefs: KibanaAdapterServiceRefs) => void) { - if (this.serviceRefs !== null) { - this.applyBufferedCalls([serviceCall]); - } else { - this.bufferedCalls.push(serviceCall); - } - } - - public applyBufferedCalls( - bufferedCalls: Array> - ) { - if (!this.serviceRefs) { - return; - } - - this.serviceRefs.rootScope.$apply(() => { - bufferedCalls.forEach((serviceCall) => { - if (!this.serviceRefs) { - return; - } - return serviceCall(this.serviceRefs); - }); - }); - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/framework/testing_framework_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/framework/testing_framework_adapter.ts deleted file mode 100644 index 46aab987fb0a42..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/framework/testing_framework_adapter.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 { FrameworkAdapter, FrameworkInfo, FrameworkUser } from './adapter_types'; - -export class TestingFrameworkAdapter implements FrameworkAdapter { - public get info() { - if (this.xpackInfo) { - return this.xpackInfo; - } else { - throw new Error('framework adapter must have init called before anything else'); - } - } - - public get currentUser() { - return this.shieldUser!; - } - private settings: any; - constructor( - private readonly xpackInfo: FrameworkInfo | null, - private readonly shieldUser: FrameworkUser | null, - public readonly version: string - ) {} - - // We dont really want to have this, but it's needed to conditionaly render for k7 due to - // when that data is needed. - public getUISetting(key: 'k7design'): boolean { - return this.settings[key]; - } - - public setUISettings = (key: string, value: any) => { - this.settings[key] = value; - }; - - public async waitUntilFrameworkReady(): Promise { - return; - } - - public registerManagementUI(settings: { sectionId?: string; name: string; order?: number }) { - throw new Error('not yet implamented'); - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts deleted file mode 100644 index db79031ade7b45..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/adapter_types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FlatObject } from '../../../frontend_types'; - -export interface RestAPIAdapter { - get(url: string, query?: FlatObject): Promise; - post(url: string, body?: { [key: string]: any }): Promise; - delete(url: string): Promise; - put(url: string, body?: any): Promise; -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts deleted file mode 100644 index acf4734ed58eea..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/axios_rest_api_adapter.ts +++ /dev/null @@ -1,79 +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 axios, { AxiosInstance } from 'axios'; -import { FlatObject } from '../../../frontend_types'; -import { RestAPIAdapter } from './adapter_types'; -let globalAPI: AxiosInstance; - -export class AxiosRestAPIAdapter implements RestAPIAdapter { - constructor(private readonly xsrfToken: string, private readonly basePath: string) {} - - public async get(url: string, query?: FlatObject): Promise { - return await this.REST.get(url, query ? { params: query } : {}).then((resp) => resp.data); - } - - public async post( - url: string, - body?: { [key: string]: any } - ): Promise { - return await this.REST.post(url, body).then((resp) => resp.data); - } - - public async delete(url: string): Promise { - return await this.REST.delete(url).then((resp) => resp.data); - } - - public async put(url: string, body?: any): Promise { - return await this.REST.put(url, body).then((resp) => resp.data); - } - - private get REST() { - if (globalAPI) { - return globalAPI; - } - - globalAPI = axios.create({ - baseURL: this.basePath, - withCredentials: true, - responseType: 'json', - timeout: 30000, - headers: { - Accept: 'application/json', - credentials: 'same-origin', - 'Content-Type': 'application/json', - 'kbn-version': this.xsrfToken, - 'kbn-xsrf': this.xsrfToken, - }, - }); - // Add a request interceptor - globalAPI.interceptors.request.use( - (config) => { - // Do something before request is sent - return config; - }, - (error) => { - // Do something with request error - return Promise.reject(error); - } - ); - - // Add a response interceptor - globalAPI.interceptors.response.use( - (response) => { - // Do something with response data - return response; - }, - (error) => { - // Do something with response error - return Promise.reject(error); - } - ); - - return globalAPI; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/node_axios_api_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/rest_api/node_axios_api_adapter.ts deleted file mode 100644 index 675f28b2edb7d8..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/rest_api/node_axios_api_adapter.ts +++ /dev/null @@ -1,93 +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 axios, { AxiosInstance } from 'axios'; -import fs from 'fs'; -import { join, resolve } from 'path'; -import { FlatObject } from '../../../frontend_types'; -import { RestAPIAdapter } from './adapter_types'; -const pkg = JSON.parse( - fs.readFileSync(resolve(join(__dirname, '../../../../../../../../package.json'))).toString() -); - -let globalAPI: AxiosInstance; - -export class NodeAxiosAPIAdapter implements RestAPIAdapter { - constructor( - private readonly username: string, - private readonly password: string, - private readonly basePath: string - ) {} - - public async get(url: string, query?: FlatObject): Promise { - return await this.REST.get(url, query ? { params: query } : {}).then((resp) => resp.data); - } - - public async post( - url: string, - body?: { [key: string]: any } - ): Promise { - return await this.REST.post(url, body).then((resp) => resp.data); - } - - public async delete(url: string): Promise { - return await this.REST.delete(url).then((resp) => resp.data); - } - - public async put(url: string, body?: any): Promise { - return await this.REST.put(url, body).then((resp) => resp.data); - } - - private get REST() { - if (globalAPI) { - return globalAPI; - } - - globalAPI = axios.create({ - baseURL: this.basePath, - withCredentials: true, - responseType: 'json', - timeout: 60 * 10 * 1000, // 10min - auth: { - username: this.username, - password: this.password, - }, - headers: { - 'Access-Control-Allow-Origin': '*', - Accept: 'application/json', - 'Content-Type': 'application/json', - 'kbn-version': (pkg as any).version, - 'kbn-xsrf': 'xxx', - }, - }); - // Add a request interceptor - globalAPI.interceptors.request.use( - (config) => { - // Do something before request is sent - return config; - }, - (error) => { - // Do something with request error - return Promise.reject(error); - } - ); - - // Add a response interceptor - globalAPI.interceptors.response.use( - (response) => { - // Do something with response data - return response; - }, - (error) => { - // Do something with response error - return Promise.reject(JSON.stringify(error.response.data)); - } - ); - - return globalAPI; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts deleted file mode 100644 index c45b354bab5d07..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/adapter_types.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { BeatTag, CMBeat } from '../../../../common/domain_types'; - -export interface CMTagsAdapter { - getTagsWithIds(tagIds: string[]): Promise; - delete(tagIds: string[]): Promise; - getAll(ESQuery?: string): Promise; - upsertTag(tag: BeatTag): Promise; - getAssignable(beats: CMBeat[]): Promise; -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts deleted file mode 100644 index bdab45c9c9d3f1..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/memory_tags_adapter.ts +++ /dev/null @@ -1,44 +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 { BeatTag, CMBeat } from '../../../../common/domain_types'; -import { CMTagsAdapter } from './adapter_types'; - -export class MemoryTagsAdapter implements CMTagsAdapter { - private tagsDB: BeatTag[] = []; - - constructor(tagsDB: BeatTag[]) { - this.tagsDB = tagsDB; - } - - public async getTagsWithIds(tagIds: string[]) { - return this.tagsDB.filter((tag) => tagIds.includes(tag.id)); - } - - public async delete(tagIds: string[]) { - this.tagsDB = this.tagsDB.filter((tag) => !tagIds.includes(tag.id)); - return true; - } - - public async getAll(ESQuery?: string) { - return this.tagsDB; - } - - public async upsertTag(tag: BeatTag) { - const existingTagIndex = this.tagsDB.findIndex((t) => t.id === tag.id); - if (existingTagIndex !== -1) { - this.tagsDB[existingTagIndex] = tag; - } else { - this.tagsDB.push(tag); - } - return tag; - } - - public async getAssignable(beats: CMBeat[]) { - return this.tagsDB; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts deleted file mode 100644 index 7c8e563948b22a..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/tags/rest_tags_adapter.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { uniq } from 'lodash'; -import { BeatTag, CMBeat } from '../../../../common/domain_types'; -import { - ReturnTypeBulkDelete, - ReturnTypeBulkGet, - ReturnTypeList, - ReturnTypeUpsert, -} from '../../../../common/return_types'; -import { RestAPIAdapter } from '../rest_api/adapter_types'; -import { CMTagsAdapter } from './adapter_types'; - -export class RestTagsAdapter implements CMTagsAdapter { - constructor(private readonly REST: RestAPIAdapter) {} - - public async getTagsWithIds(tagIds: string[]): Promise { - try { - return ( - await this.REST.get>(`/api/beats/tags/${uniq(tagIds).join(',')}`) - ).items; - } catch (e) { - return []; - } - } - - public async getAll(ESQuery: string): Promise { - try { - return (await this.REST.get>(`/api/beats/tags`, { ESQuery })).list; - } catch (e) { - return []; - } - } - - public async delete(tagIds: string[]): Promise { - return ( - await this.REST.delete(`/api/beats/tags/${uniq(tagIds).join(',')}`) - ).success; - } - - public async upsertTag(tag: BeatTag): Promise { - const response = await this.REST.put>(`/api/beats/tag/${tag.id}`, { - color: tag.color, - name: tag.name, - }); - - return response.success ? tag : null; - } - - public async getAssignable(beats: CMBeat[]) { - try { - return ( - await this.REST.get>( - `/api/beats/tags/assignable/${beats.map((beat) => beat.id).join(',')}` - ) - ).items; - } catch (e) { - return []; - } - } -} diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts b/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts deleted file mode 100644 index cac45c6b9f674c..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/adapters/tokens/rest_tokens_adapter.ts +++ /dev/null @@ -1,23 +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 { ReturnTypeBulkCreate } from '../../../../common/return_types'; -import { RestAPIAdapter } from '../rest_api/adapter_types'; -import { CMTokensAdapter } from './adapter_types'; - -export class RestTokensAdapter implements CMTokensAdapter { - constructor(private readonly REST: RestAPIAdapter) {} - - public async createEnrollmentTokens(numTokens: number = 1): Promise { - const results = ( - await this.REST.post>('/api/beats/enrollment_tokens', { - num_tokens: numTokens, - }) - ).results; - return results.map((result) => result.item); - } -} diff --git a/x-pack/plugins/beats_management/public/lib/beats.ts b/x-pack/plugins/beats_management/public/lib/beats.ts deleted file mode 100644 index ebe2f4125ba851..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/beats.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ReturnTypeBulkAction } from '../../common/return_types'; -import { CMBeat } from '../../common/domain_types'; -import { BeatsTagAssignment, CMBeatsAdapter } from './adapters/beats/adapter_types'; -import { ElasticsearchLib } from './elasticsearch'; - -export class BeatsLib { - constructor( - private readonly adapter: CMBeatsAdapter, - private readonly elasticsearch: ElasticsearchLib - ) {} - - /** Get a single beat using it's ID for lookup */ - public async get(id: string): Promise { - const beat = await this.adapter.get(id); - return beat; - } - - /** Get a single beat using the token it was enrolled in for lookup */ - public getBeatWithToken = async (enrollmentToken: string): Promise => { - const beat = await this.adapter.getBeatWithToken(enrollmentToken); - return beat; - }; - - /** Get an array of beats that have a given tag id assigned to it */ - public getBeatsWithTag = async (tagId: string): Promise => { - const beats = await this.adapter.getBeatsWithTag(tagId); - return beats; - }; - - // FIXME: This needs to be paginated https://github.com/elastic/kibana/issues/26022 - /** Get an array of all enrolled beats. */ - public getAll = async (kuery?: string): Promise => { - let ESQuery; - if (kuery) { - ESQuery = await this.elasticsearch.convertKueryToEsQuery(kuery); - } - const beats = await this.adapter.getAll(ESQuery); - return beats; - }; - - /** Update a given beat via it's ID */ - public update = async (id: string, beatData: Partial): Promise => { - return await this.adapter.update(id, beatData); - }; - - /** unassign tags from beats using an array of tags and beats */ - public removeTagsFromBeats = async ( - removals: BeatsTagAssignment[] - ): Promise => { - return await this.adapter.removeTagsFromBeats(removals); - }; - - /** assign tags from beats using an array of tags and beats */ - public assignTagsToBeats = async ( - assignments: BeatsTagAssignment[] - ): Promise => { - return await this.adapter.assignTagsToBeats(assignments); - }; -} diff --git a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts b/x-pack/plugins/beats_management/public/lib/compose/kibana.ts deleted file mode 100644 index bd562f03548c26..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/compose/kibana.ts +++ /dev/null @@ -1,81 +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 { camelCase } from 'lodash'; -import { configBlockSchemas } from '../../../common/config_schemas'; -import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; -import { INDEX_NAMES } from '../../../common/constants/index_names'; -import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; -import { RestConfigBlocksAdapter } from '../adapters/configuration_blocks/rest_config_blocks_adapter'; -import { RestElasticsearchAdapter } from '../adapters/elasticsearch/rest'; -import { KibanaFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; -import { AxiosRestAPIAdapter } from '../adapters/rest_api/axios_rest_api_adapter'; -import { RestTagsAdapter } from '../adapters/tags/rest_tags_adapter'; -import { RestTokensAdapter } from '../adapters/tokens/rest_tokens_adapter'; -import { BeatsLib } from '../beats'; -import { ConfigBlocksLib } from '../configuration_blocks'; -import { ElasticsearchLib } from '../elasticsearch'; -import { TagsLib } from '../tags'; -import { FrontendLibs } from '../types'; -import { PLUGIN } from '../../../common/constants/plugin'; -import { FrameworkLib } from './../framework'; -import { ManagementSetup } from '../../../../../../src/plugins/management/public'; -import { SecurityPluginSetup } from '../../../../security/public'; -import { CoreSetup } from '../../../../../../src/core/public'; -import { LicensingPluginSetup } from '../../../../licensing/public'; -import { BeatsManagementConfigType } from '../../../common'; - -interface ComposeDeps { - core: CoreSetup; - management: ManagementSetup; - licensing: LicensingPluginSetup; - config: BeatsManagementConfigType; - version: string; - security?: SecurityPluginSetup; -} - -export function compose({ - core, - management, - licensing, - config, - version, - security, -}: ComposeDeps): FrontendLibs { - const api = new AxiosRestAPIAdapter(version, core.http.basePath.get()); - const esAdapter = new RestElasticsearchAdapter(INDEX_NAMES.BEATS); - const elasticsearchLib = new ElasticsearchLib(esAdapter); - const configBlocks = new ConfigBlocksLib( - new RestConfigBlocksAdapter(api), - translateConfigSchema(configBlockSchemas) - ); - const tags = new TagsLib(new RestTagsAdapter(api), elasticsearchLib); - const tokens = new RestTokensAdapter(api); - const beats = new BeatsLib(new RestBeatsAdapter(api), elasticsearchLib); - - const framework = new FrameworkLib( - new KibanaFrameworkAdapter( - camelCase(PLUGIN.ID), - management, - core.http.basePath.get, - licensing, - security, - config, - version - ) - ); - - const libs: FrontendLibs = { - framework, - elasticsearch: elasticsearchLib, - tags, - tokens, - beats, - configBlocks, - }; - return libs; -} diff --git a/x-pack/plugins/beats_management/public/lib/compose/scripts.ts b/x-pack/plugins/beats_management/public/lib/compose/scripts.ts deleted file mode 100644 index 37a1bc9ac4944d..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/compose/scripts.ts +++ /dev/null @@ -1,79 +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 { configBlockSchemas } from '../../../common/config_schemas'; -import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; -import { RestBeatsAdapter } from '../adapters/beats/rest_beats_adapter'; -import { RestConfigBlocksAdapter } from '../adapters/configuration_blocks/rest_config_blocks_adapter'; -import { MemoryElasticsearchAdapter } from '../adapters/elasticsearch/memory'; -import { TestingFrameworkAdapter } from '../adapters/framework/testing_framework_adapter'; -import { NodeAxiosAPIAdapter } from '../adapters/rest_api/node_axios_api_adapter'; -import { RestTagsAdapter } from '../adapters/tags/rest_tags_adapter'; -import { RestTokensAdapter } from '../adapters/tokens/rest_tokens_adapter'; -import { BeatsLib } from '../beats'; -import { ConfigBlocksLib } from '../configuration_blocks'; -import { ElasticsearchLib } from '../elasticsearch'; -import { FrameworkLib } from '../framework'; -import { TagsLib } from '../tags'; -import { FrontendLibs } from '../types'; - -export function compose(basePath: string): FrontendLibs { - const api = new NodeAxiosAPIAdapter('elastic', 'changeme', basePath); - const esAdapter = new MemoryElasticsearchAdapter( - () => true, - () => '', - [] - ); - const elasticsearchLib = new ElasticsearchLib(esAdapter); - const configBlocks = new ConfigBlocksLib( - new RestConfigBlocksAdapter(api), - translateConfigSchema(configBlockSchemas) - ); - const tags = new TagsLib(new RestTagsAdapter(api), elasticsearchLib); - const tokens = new RestTokensAdapter(api); - const beats = new BeatsLib(new RestBeatsAdapter(api), elasticsearchLib); - - const framework = new FrameworkLib( - new TestingFrameworkAdapter( - { - basePath, - license: { - type: 'gold', - expired: false, - expiry_date_in_millis: 34353453452345, - }, - security: { - enabled: true, - available: true, - }, - settings: { - encryptionKey: 'xpack_beats_default_encryptionKey', - enrollmentTokensTtlInSeconds: 10 * 60, // 10 minutes - defaultUserRoles: ['superuser'], - }, - }, - { - username: 'joeuser', - roles: ['beats_admin'], - enabled: true, - full_name: null, - email: null, - }, - '6.7.0' - ) - ); - - const libs: FrontendLibs = { - framework, - elasticsearch: elasticsearchLib, - tags, - tokens, - beats, - configBlocks, - }; - return libs; -} diff --git a/x-pack/plugins/beats_management/public/lib/config_blocks.test.ts b/x-pack/plugins/beats_management/public/lib/config_blocks.test.ts deleted file mode 100644 index 5fcd30e32d0236..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/config_blocks.test.ts +++ /dev/null @@ -1,149 +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 { configBlockSchemas } from '../../common/config_schemas'; -import { translateConfigSchema } from '../../common/config_schemas_translations_map'; -import { ConfigBlocksLib } from './configuration_blocks'; -import { MemoryConfigBlocksAdapter } from './adapters/configuration_blocks/memory_config_blocks_adapter'; - -describe('Tags Client Domain Lib', () => { - let lib: ConfigBlocksLib; - - beforeEach(async () => { - lib = new ConfigBlocksLib( - new MemoryConfigBlocksAdapter([]), - translateConfigSchema(configBlockSchemas) - ); - }); - - it('should use helper function to convert users yaml in tag to config object', async () => { - const convertedBlocks = await lib.userConfigsToJson([ - { - id: 'foo', - tag: 'basic', - last_updated: parseInt(new Date().toISOString(), 10), - type: 'filebeat.inputs', - description: 'string', - config: { - paths: ['adad/adasd'], - other: "something: 'here'", - }, - }, - ]); - - expect(convertedBlocks.length).toBe(1); - expect(convertedBlocks[0]).toHaveProperty('config'); - expect(convertedBlocks[0].config).not.toHaveProperty('other'); - expect(convertedBlocks[0].config).toHaveProperty('something'); - expect(convertedBlocks[0].config.something).toBe('here'); - }); - - it('should use helper function to convert user config to json with undefined `other`', async () => { - const convertedTag = await lib.userConfigsToJson([ - { - id: 'foo', - tag: 'basic', - last_updated: parseInt(new Date().toISOString(), 10), - type: 'filebeat.inputs', - description: 'sdfsdf', - config: { - paths: ['sdfsfsdf'], - other: undefined, - }, - }, - ]); - - expect(convertedTag.length).toBe(1); - expect(convertedTag[0]).not.toHaveProperty('other'); - }); - - it('should use helper function to convert users yaml in tag to config object, where empty other leads to no other fields saved', async () => { - const convertedTag = await lib.userConfigsToJson([ - { - id: 'foo', - tag: 'basic', - last_updated: parseInt(new Date().toISOString(), 10), - type: 'filebeat.inputs', - description: 'string', - config: { - paths: ['adad/adasd'], - other: ` - sdfsdf: "foo" - `, - }, - }, - ]); - - expect(convertedTag.length).toBe(1); - expect(convertedTag[0].config).not.toHaveProperty('other'); - expect(convertedTag[0].config.sdfsdf).toBe('foo'); - }); - - it('should convert tokenized fields to JSON', async () => { - const convertedTag = await lib.userConfigsToJson([ - { - id: 'foo', - tag: 'basic', - last_updated: parseInt(new Date().toISOString(), 10), - type: 'output', - description: 'something', - config: { - _sub_type: 'console', - hosts: ['esefsfsgg', 'drgdrgdgr'], - username: '', - password: '', - }, - }, - ]); - - expect(convertedTag.length).toBe(1); - expect(convertedTag[0].config).toHaveProperty('_sub_type'); - expect(convertedTag[0].config).toHaveProperty('hosts'); - expect(convertedTag[0].config.hosts.length).toBe(2); - }); - - it('should use helper function to convert config object to users yaml', async () => { - const convertedTag = await lib.jsonConfigToUserYaml([ - { - id: 'foo', - tag: 'basic', - last_updated: parseInt(new Date().toISOString(), 10), - type: 'filebeat.inputs', - description: 'sdfsdf', - config: { - paths: ['sdfsfsdf'], - something: 'here', - }, - }, - ]); - - expect(convertedTag.length).toBe(1); - expect(convertedTag[0].config).not.toHaveProperty('something'); - expect(convertedTag[0].config).toHaveProperty('other'); - expect(convertedTag[0].config.other).toBe('something: here\n'); - }); - - it('should use helper function to convert config object to users yaml with empty `other`', async () => { - const convertedTag = await lib.jsonConfigToUserYaml([ - { - id: 'foo', - tag: 'basic', - last_updated: parseInt(new Date().toISOString(), 10), - type: 'filebeat.inputs', - description: undefined, - config: { - paths: ['sdfsfsdf'], - }, - }, - ]); - - expect(convertedTag.length).toBe(1); - expect(convertedTag[0].config).not.toHaveProperty('something'); - expect(convertedTag[0].config).toHaveProperty('other'); - expect(convertedTag[0].config.other).toBe(''); - }); -}); diff --git a/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts b/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts deleted file mode 100644 index 2328b353fe326f..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/configuration_blocks.ts +++ /dev/null @@ -1,123 +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 { set } from '@elastic/safer-lodash-set'; -import { get, has, omit } from 'lodash'; -import { ConfigBlockSchema, ConfigurationBlock } from '../../common/domain_types'; -import { FrontendConfigBlocksAdapter } from './adapters/configuration_blocks/adapter_types'; - -export class ConfigBlocksLib { - public delete = this.adapter.delete.bind(this.adapter); - - constructor( - private readonly adapter: FrontendConfigBlocksAdapter, - private readonly configSchemas: ConfigBlockSchema[] - ) {} - - public upsert = async (blocks: ConfigurationBlock[]) => { - return await this.adapter.upsert(await this.userConfigsToJson(blocks)); - }; - - public getForTags = async (tagIds: string[], page: number) => { - const result = await this.adapter.getForTags(tagIds, page); - result.list = await this.jsonConfigToUserYaml(result.list); - return result; - }; - - public async jsonConfigToUserYaml(blocks: ConfigurationBlock[]): Promise { - const yaml = await import('js-yaml'); - // configuration_blocks yaml, JS cant read YAML so we parse it into JS, - // because beats flattens all fields, and we need more structure. - // we take tagConfigs, grab the config that applies here, render what we can into - // an object, and the rest we assume to be the yaml string that goes - // into the yaml editor... - // NOTE: The perk of this, is that as we support more features via controls - // vs yaml editing, it should "just work", and things that were in YAML - // will now be in the UI forms... - return blocks.map((block) => { - const { type, config } = block; - - const thisConfigSchema = this.configSchemas.find((conf) => conf.id === type); - const thisConfigBlockSchema = thisConfigSchema ? thisConfigSchema.configs : null; - if (!thisConfigBlockSchema) { - throw new Error('No config block schema '); - } - - const knownConfigIds: string[] = thisConfigBlockSchema.map((schema) => schema.id); - - const convertedConfig: ConfigurationBlock['config'] = knownConfigIds.reduce( - (blockObj: any, configKey: string, index: number) => { - const unhydratedKey = knownConfigIds[index]; - - set(blockObj, configKey, get(config, unhydratedKey)); - - return blockObj; - }, - thisConfigSchema && thisConfigSchema.allowOtherConfigs - ? { other: yaml.safeDump(omit(config, knownConfigIds)) } - : {} - ); - - // Workaround to empty object passed into dump resulting in this odd output - if (convertedConfig.other && convertedConfig.other === '{}\n') { - convertedConfig.other = ''; - } - - return { - ...block, - config: convertedConfig, - }; - }); - } - - public async userConfigsToJson(blocks: ConfigurationBlock[]): Promise { - const yaml = await import('js-yaml'); - // configurations is the JS representation of the config yaml, - // so here we take that JS and convert it into a YAML string. - // we do so while also flattening "other" into the flat yaml beats expect - return blocks.map((block) => { - const { type, config } = block; - const thisConfigSchema = this.configSchemas.find((conf) => conf.id === type); - const thisConfigBlockSchema = thisConfigSchema ? thisConfigSchema.configs : null; - if (!thisConfigBlockSchema) { - throw new Error('No config block schema '); - } - const knownConfigIds = thisConfigBlockSchema - .map((schema: ConfigurationBlock['config']) => schema.id) - .filter((id: string) => id !== 'other'); - - const picked = this.pickDeep(config, knownConfigIds); - let other = yaml.safeLoad(config.other || '{}'); - if (typeof other === 'string') { - other = { - [other]: '', - }; - } - - const convertedConfig = { - ...other, - ...picked, - }; - - return { - ...block, - config: convertedConfig, - }; - }); - } - - private pickDeep(obj: { [key: string]: any }, keys: string[]) { - const copy = {}; - keys.forEach((key) => { - if (has(obj, key)) { - const val = get(obj, key); - set(copy, key, val); - } - }); - return copy; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/elasticsearch.ts b/x-pack/plugins/beats_management/public/lib/elasticsearch.ts deleted file mode 100644 index b64c307682f2f7..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/elasticsearch.ts +++ /dev/null @@ -1,70 +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 { QuerySuggestion } from '../../../../../src/plugins/data/public'; -import { ElasticsearchAdapter } from './adapters/elasticsearch/adapter_types'; - -interface HiddenFields { - op: 'is' | 'startsWith' | 'withoutPrefix'; - value: string; -} - -export class ElasticsearchLib { - private readonly hiddenFields: HiddenFields[] = [ - { op: 'startsWith', value: 'enrollment_token' }, - { op: 'is', value: 'beat.active' }, - { op: 'is', value: 'beat.enrollment_token' }, - { op: 'is', value: 'beat.access_token' }, - { op: 'is', value: 'beat.ephemeral_id' }, - { op: 'is', value: 'beat.verified_on' }, - ]; - - constructor(private readonly adapter: ElasticsearchAdapter) {} - - public isKueryValid(kuery: string): boolean { - return this.adapter.isKueryValid(kuery); - } - public async convertKueryToEsQuery(kuery: string): Promise { - return await this.adapter.convertKueryToEsQuery(kuery); - } - - public async getSuggestions( - kuery: string, - selectionStart: any, - fieldPrefix?: string - ): Promise { - const suggestions = await this.adapter.getSuggestions(kuery, selectionStart); - - const filteredSuggestions = suggestions.filter((suggestion) => { - const hiddenFieldsCheck = this.hiddenFields; - - if (fieldPrefix) { - hiddenFieldsCheck.push({ - op: 'withoutPrefix', - value: `${fieldPrefix}.`, - }); - } - - return hiddenFieldsCheck.reduce((isvalid: boolean, field) => { - if (!isvalid) { - return false; - } - - switch (field.op) { - case 'startsWith': - return !suggestion.text.startsWith(field.value); - case 'is': - return suggestion.text.trim() !== field.value; - case 'withoutPrefix': - return suggestion.text.startsWith(field.value); - } - }, true); - }); - - return filteredSuggestions; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/framework.ts b/x-pack/plugins/beats_management/public/lib/framework.ts deleted file mode 100644 index 94f6df20e4b4d3..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/framework.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { difference, get } from 'lodash'; -import { LICENSES, LicenseType } from '../../common/constants/security'; -import { FrameworkAdapter } from './adapters/framework/adapter_types'; - -export class FrameworkLib { - public waitUntilFrameworkReady = this.adapter.waitUntilFrameworkReady.bind(this.adapter); - public registerManagementUI = this.adapter.registerManagementUI.bind(this.adapter); - - constructor(private readonly adapter: FrameworkAdapter) {} - - public get currentUser() { - return this.adapter.currentUser; - } - - public get info() { - return this.adapter.info; - } - - public licenseIsAtLeast(type: LicenseType) { - return ( - LICENSES.indexOf(get(this.adapter.info, 'license.type', 'oss')) >= LICENSES.indexOf(type) - ); - } - - public versionGreaterThen(version: string) { - const pa = this.adapter.version.split('.'); - const pb = version.split('.'); - for (let i = 0; i < 3; i++) { - const na = Number(pa[i]); - const nb = Number(pb[i]); - // version is greater - if (na > nb) { - return true; - } - // version is less then - if (nb > na) { - return false; - } - if (!isNaN(na) && isNaN(nb)) { - return true; - } - if (isNaN(na) && !isNaN(nb)) { - return false; - } - } - return true; - } - - public currentUserHasOneOfRoles(roles: string[]) { - // If the user has at least one of the roles requested, the returnd difference will be less - // then the orig array size. difference only compares based on the left side arg - return difference(roles, get(this.currentUser, 'roles', []) as string[]).length < roles.length; - } -} diff --git a/x-pack/plugins/beats_management/public/lib/tags.ts b/x-pack/plugins/beats_management/public/lib/tags.ts deleted file mode 100644 index 45f840be8e0627..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/tags.ts +++ /dev/null @@ -1,48 +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 uuidv4 from 'uuid/v4'; -import { BeatTag, CMBeat } from '../../common/domain_types'; -import { CMTagsAdapter } from './adapters/tags/adapter_types'; -import { ElasticsearchLib } from './elasticsearch'; - -export class TagsLib { - constructor( - private readonly adapter: CMTagsAdapter, - private readonly elasticsearch: ElasticsearchLib - ) {} - - public async getTagsWithIds(tagIds: string[]): Promise { - if (tagIds.length === 0) { - return []; - } - return await this.adapter.getTagsWithIds([...new Set(tagIds)]); - } - public async delete(tagIds: string[]): Promise { - return await this.adapter.delete([...new Set(tagIds)]); - } - - // FIXME: This needs to be paginated https://github.com/elastic/kibana/issues/26022 - public async getAll(kuery?: string): Promise { - let ESQuery; - if (kuery) { - ESQuery = await this.elasticsearch.convertKueryToEsQuery(kuery); - } - - return await this.adapter.getAll(ESQuery); - } - public async upsertTag(tag: BeatTag): Promise { - if (!tag.id) { - tag.id = uuidv4(); - } - return await this.adapter.upsertTag(tag); - } - - public async getassignableTagsForBeats(beats: CMBeat[]): Promise { - return await this.adapter.getAssignable(beats); - } -} diff --git a/x-pack/plugins/beats_management/public/lib/types.ts b/x-pack/plugins/beats_management/public/lib/types.ts deleted file mode 100644 index 68f09cc3e8ba43..00000000000000 --- a/x-pack/plugins/beats_management/public/lib/types.ts +++ /dev/null @@ -1,60 +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 { IModule, IScope } from 'angular'; -import { AxiosRequestConfig } from 'axios'; -import { FrameworkAdapter } from './adapters/framework/adapter_types'; -import { CMTokensAdapter } from './adapters/tokens/adapter_types'; -import { BeatsLib } from './beats'; -import { ConfigBlocksLib } from './configuration_blocks'; -import { ElasticsearchLib } from './elasticsearch'; -import { FrameworkLib } from './framework'; -import { TagsLib } from './tags'; - -export interface FrontendLibs { - elasticsearch: ElasticsearchLib; - framework: FrameworkLib; - beats: BeatsLib; - tags: TagsLib; - tokens: CMTokensAdapter; - configBlocks: ConfigBlocksLib; -} - -export type FramworkAdapterConstructable = new (uiModule: IModule) => FrameworkAdapter; - -// FIXME: replace AxiosRequestConfig with something more defined -export type RequestConfig = AxiosRequestConfig; - -export interface ApiAdapter { - kbnVersion: string; - - get(url: string, config?: RequestConfig | undefined): Promise; - post(url: string, data?: any, config?: AxiosRequestConfig | undefined): Promise; - delete(url: string, config?: RequestConfig | undefined): Promise; - put(url: string, data?: any, config?: RequestConfig | undefined): Promise; -} - -export interface UiKibanaAdapterScope extends IScope { - breadcrumbs: any[]; - topNavMenu: any[]; -} - -export interface KibanaUIConfig { - get(key: string): any; - set(key: string, value: any): Promise; -} - -export interface KibanaAdapterServiceRefs { - config: KibanaUIConfig; - rootScope: IScope; -} - -export type BufferedKibanaServiceCall = (serviceRefs: ServiceRefs) => void; - -export interface Chrome { - setRootTemplate(template: string): void; -} diff --git a/x-pack/plugins/beats_management/public/pages/__404.tsx b/x-pack/plugins/beats_management/public/pages/__404.tsx deleted file mode 100644 index d5813d36e2c6d3..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/__404.tsx +++ /dev/null @@ -1,22 +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 { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -export class NotFoundPage extends React.PureComponent { - public render() { - return ( -
- -
- ); - } -} diff --git a/x-pack/plugins/beats_management/public/pages/beat/details.tsx b/x-pack/plugins/beats_management/public/pages/beat/details.tsx deleted file mode 100644 index c9b26da9349b17..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/beat/details.tsx +++ /dev/null @@ -1,201 +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 { - EuiFlexGroup, - EuiFlexItem, - // @ts-ignore EuiInMemoryTable typings not yet available - EuiInMemoryTable, - EuiLink, - EuiSpacer, - EuiText, - EuiTitle, - EuiBasicTableColumn, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React from 'react'; -import { configBlockSchemas } from '../../../common/config_schemas'; -import { translateConfigSchema } from '../../../common/config_schemas_translations_map'; -import { TABLE_CONFIG } from '../../../common/constants'; -import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; -import { Breadcrumb } from '../../components/navigation/breadcrumb'; -import { ConnectedLink } from '../../components/navigation/connected_link'; -import { TagBadge } from '../../components/tag'; -import { ConfigView } from '../../components/tag/config_view/index'; -import { AppPageProps } from '../../frontend_types'; - -interface PageProps extends AppPageProps { - beat: CMBeat; - intl: InjectedIntl; -} - -interface PageState { - selectedConfig: ConfigurationBlock | null; - tags: BeatTag[]; - configuration_blocks: ConfigurationBlock[]; - configurationBlocksPage: number; -} - -class BeatDetailPageUi extends React.PureComponent { - constructor(props: PageProps) { - super(props); - - this.state = { - selectedConfig: null, - tags: [], - configuration_blocks: [], - configurationBlocksPage: 0, - }; - } - - public async UNSAFE_componentWillMount() { - const tags = await this.props.libs.tags.getTagsWithIds(this.props.beat.tags); - const blocksResult = await this.props.libs.configBlocks.getForTags( - this.props.beat.tags, - this.state.configurationBlocksPage - ); - - this.setState({ - configuration_blocks: blocksResult.list, - tags, - }); - } - public render() { - const props = this.props; - const { beat, intl } = props; - if (!beat) { - return ( - - ); - } - const configurationBlocks = !this.state.configuration_blocks - ? [] - : this.state.configuration_blocks.map((configuration) => ({ - // @ts-ignore one of the types on ConfigurationBlock doesn't define a "module" property - module: configuration.config.type || null, - tagId: configuration.tag, - tagColor: - ((this.state.tags || []).find((tag) => tag.id === configuration.tag) || ({} as BeatTag)) - .color || 'grey', - tagName: - ((this.state.tags || []).find((tag) => tag.id === configuration.tag) || ({} as BeatTag)) - .name || configuration.tag, - ...beat, - ...configuration, - displayValue: get( - translateConfigSchema(configBlockSchemas).find( - (config) => config.id === configuration.type - ), - 'text', - null - ), - })); - - const columns: Array> = [ - { - field: 'displayValue', - name: intl.formatMessage({ - id: 'xpack.beatsManagement.beatConfigurations.typeColumnName', - defaultMessage: 'Type', - }), - sortable: true, - render: (value: string | null, configuration: any) => ( - { - this.setState({ - selectedConfig: configuration, - }); - }} - > - {value || configuration.type} - - ), - }, - { - field: 'module', - name: intl.formatMessage({ - id: 'xpack.beatsManagement.beatConfigurations.moduleColumnName', - defaultMessage: 'Module', - }), - sortable: true, - }, - { - field: 'description', - name: intl.formatMessage({ - id: 'xpack.beatsManagement.beatConfigurations.descriptionColumnName', - defaultMessage: 'Description', - }), - sortable: true, - }, - { - field: 'tagId', - name: intl.formatMessage({ - id: 'xpack.beatsManagement.beatConfigurations.tagColumnName', - defaultMessage: 'Tag', - }), - render: (id: string, block: any) => ( - - - - ), - sortable: true, - }, - ]; - return ( - - - - - - -

- -

-
- -

- -

-
-
- - - -
- {this.state.selectedConfig && ( - this.setState({ selectedConfig: null })} - /> - )} -
- ); - } -} -export const BeatDetailPage = injectI18n(BeatDetailPageUi); diff --git a/x-pack/plugins/beats_management/public/pages/beat/index.tsx b/x-pack/plugins/beats_management/public/pages/beat/index.tsx deleted file mode 100644 index 3b306d6699a44d..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/beat/index.tsx +++ /dev/null @@ -1,201 +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 { - EuiFlexGroup, - EuiFlexItem, - // @ts-ignore - EuiTab, - // @ts-ignore - EuiTabs, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import moment from 'moment'; -import React from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import { CMBeat } from '../../../common/domain_types'; -import { PrimaryLayout } from '../../components/layouts/primary'; -import { Breadcrumb } from '../../components/navigation/breadcrumb'; -import { ChildRoutes } from '../../components/navigation/child_routes'; -import { AppPageProps } from '../../frontend_types'; - -interface PageProps extends AppPageProps { - intl: InjectedIntl; -} -interface PageState { - beat: CMBeat | undefined; - beatId: string; - isLoading: boolean; -} - -class BeatDetailsPageComponent extends React.PureComponent { - constructor(props: PageProps) { - super(props); - this.state = { - beat: undefined, - beatId: props.match.params.beatId, - isLoading: true, - }; - this.loadBeat(); - } - - public onSelectedTabChanged = (id: string) => { - this.props.history.push({ - pathname: id, - search: this.props.location.search, - }); - }; - - public renderActionSection(beat?: CMBeat) { - return beat ? ( - - - - {beat.type} }} - /> - - - - - {beat.version} }} - /> - - - {beat.last_updated && ( - - - {moment(beat.last_updated).fromNow()}, - }} - /> - - - )} - - ) : ( - - ); - } - - public render() { - const { intl } = this.props; - const { beat } = this.state; - let id: string | undefined; - let name; - - if (beat) { - id = beat.id; - name = beat.name; - } - - const title = this.state.isLoading - ? intl.formatMessage({ - id: 'xpack.beatsManagement.beat.loadingTitle', - defaultMessage: 'Loading', - }) - : intl.formatMessage( - { - id: 'xpack.beatsManagement.beat.beatNameAndIdTitle', - defaultMessage: 'Beat: {nameOrNoName} (id: {id})', - }, - { - nameOrNoName: - name || - intl.formatHTMLMessage({ - id: 'xpack.beatsManagement.beat.noNameReceivedFromBeatTitle', - defaultMessage: 'No name received from beat', - }), - id, - } - ); - - return ( - - - - - - - - - - - - {!this.state.beat &&
Beat not found
} - {this.state.beat && ( - - - {id && } />} - - )} -
-
- ); - } - - private onTabClicked = (path: string) => { - return () => { - this.props.goTo(path); - }; - }; - - private async loadBeat() { - const { intl } = this.props; - const { beatId } = this.props.match.params; - let beat; - try { - beat = await this.props.libs.beats.get(beatId); - if (!beat) { - throw new Error( - intl.formatMessage({ - id: 'xpack.beatsManagement.beat.beatNotFoundErrorMessage', - defaultMessage: 'beat not found', - }) - ); - } - } catch (e) { - throw new Error(e); - } - this.setState({ beat, isLoading: false }); - } -} - -export const BeatDetailsPage = injectI18n(BeatDetailsPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/beat/tags.tsx b/x-pack/plugins/beats_management/public/pages/beat/tags.tsx deleted file mode 100644 index e5f7e7c3711a05..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/beat/tags.tsx +++ /dev/null @@ -1,76 +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 { EuiGlobalToastList } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { BeatTag, CMBeat } from '../../../common/domain_types'; -import { Breadcrumb } from '../../components/navigation/breadcrumb'; -import { BeatDetailTagsTable, Table } from '../../components/table'; -import { FrontendLibs } from '../../lib/types'; - -interface BeatTagsPageProps { - beat: CMBeat; - libs: FrontendLibs; - refreshBeat(): void; -} - -interface BeatTagsPageState { - notifications: any[]; - tags: BeatTag[]; -} - -export class BeatTagsPage extends React.PureComponent { - private tableRef = React.createRef
(); - constructor(props: BeatTagsPageProps) { - super(props); - - this.state = { - notifications: [], - tags: [], - }; - } - - public UNSAFE_componentWillMount() { - this.updateBeatsData(); - } - - public async updateBeatsData() { - const tags = await this.props.libs.tags.getTagsWithIds(this.props.beat.tags); - this.setState({ - tags, - }); - } - - public render() { - const { beat } = this.props; - return ( - - - -
- - this.setState({ notifications: [] })} - toastLifeTimeMs={5000} - /> - - ); - } -} diff --git a/x-pack/plugins/beats_management/public/pages/error/enforce_security.tsx b/x-pack/plugins/beats_management/public/pages/error/enforce_security.tsx deleted file mode 100644 index 2d55df7be69230..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/error/enforce_security.tsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import * as React from 'react'; -import { NoDataLayout } from '../../components/layouts/no_data'; - -export const EnforceSecurityPage = injectI18n(({ intl }) => ( - -

- -

-
-)); diff --git a/x-pack/plugins/beats_management/public/pages/error/invalid_license.tsx b/x-pack/plugins/beats_management/public/pages/error/invalid_license.tsx deleted file mode 100644 index d34e3d22e4e677..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/error/invalid_license.tsx +++ /dev/null @@ -1,28 +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 { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import * as React from 'react'; -import { NoDataLayout } from '../../components/layouts/no_data'; - -export const InvalidLicensePage = injectI18n(({ intl }) => ( - -

- -

-
-)); diff --git a/x-pack/plugins/beats_management/public/pages/error/no_access.tsx b/x-pack/plugins/beats_management/public/pages/error/no_access.tsx deleted file mode 100644 index 2a380a64ccd898..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/error/no_access.tsx +++ /dev/null @@ -1,29 +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 { FormattedMessage, injectI18n } from '@kbn/i18n/react'; -import * as React from 'react'; -import { NoDataLayout } from '../../components/layouts/no_data'; - -export const NoAccessPage = injectI18n(({ intl }) => ( - -

- -

-
-)); diff --git a/x-pack/plugins/beats_management/public/pages/index.ts b/x-pack/plugins/beats_management/public/pages/index.ts deleted file mode 100644 index eafe9f9b1394fe..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/index.ts +++ /dev/null @@ -1,56 +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 { BeatDetailPage } from './beat/details'; -import { BeatDetailsPage } from './beat/index'; -import { BeatTagsPage } from './beat/tags'; -import { EnforceSecurityPage } from './error/enforce_security'; -import { InvalidLicensePage } from './error/invalid_license'; -import { NoAccessPage } from './error/no_access'; -import { TagsPage } from './overview/configuration_tags'; -import { BeatsPage } from './overview/enrolled_beats'; -import { MainPage } from './overview/index'; -import { TagCreatePage } from './tag/create'; -import { TagEditPage } from './tag/edit'; -import { BeatsInitialEnrollmentPage } from './walkthrough/initial/beat'; -import { FinishWalkthroughPage } from './walkthrough/initial/finish'; -import { InitialWalkthroughPage } from './walkthrough/initial/index'; -import { InitialTagPage } from './walkthrough/initial/tag'; -import type { RouteConfig } from '../components/navigation/child_routes'; - -export const routeMap: RouteConfig[] = [ - { path: '/tag/create/:tagid?', component: TagCreatePage }, - { path: '/tag/edit/:tagid?', component: TagEditPage }, - { - path: '/beat/:beatId', - component: BeatDetailsPage, - routes: [ - { path: '/beat/:beatId/details', component: BeatDetailPage }, - { path: '/beat/:beatId/tags', component: BeatTagsPage }, - ], - }, - { path: '/error/enforce_security', component: EnforceSecurityPage }, - { path: '/error/invalid_license', component: InvalidLicensePage }, - { path: '/error/no_access', component: NoAccessPage }, - { - path: '/overview', - component: MainPage, - routes: [ - { path: '/overview/configuration_tags', component: TagsPage }, - { path: '/overview/enrolled_beats', component: BeatsPage }, - ], - }, - { - path: '/walkthrough/initial', - component: InitialWalkthroughPage, - routes: [ - { path: '/walkthrough/initial/beat', component: BeatsInitialEnrollmentPage }, - { path: '/walkthrough/initial/finish', component: FinishWalkthroughPage }, - { path: '/walkthrough/initial/tag', component: InitialTagPage }, - ], - }, -]; diff --git a/x-pack/plugins/beats_management/public/pages/overview/configuration_tags.tsx b/x-pack/plugins/beats_management/public/pages/overview/configuration_tags.tsx deleted file mode 100644 index 0b7c4f17a6e2ca..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/overview/configuration_tags.tsx +++ /dev/null @@ -1,118 +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 { EuiButton } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React from 'react'; -import { Breadcrumb } from '../../components/navigation/breadcrumb'; -import { AssignmentActionType, Table, TagsTableType } from '../../components/table'; -import { tagListActions } from '../../components/table/action_schema'; -import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; -import { AppPageProps } from '../../frontend_types'; - -interface PageProps extends AppPageProps { - renderAction: (area: () => JSX.Element) => void; - intl: InjectedIntl; -} - -interface PageState { - tableRef: any; -} - -class TagsPageComponent extends React.PureComponent { - constructor(props: PageProps) { - super(props); - - this.state = { - tableRef: React.createRef(), - }; - - props.containers.tags.reload(props.urlState.tagsKBar); - props.renderAction(this.renderActionArea); - } - - public renderActionArea = () => ( - { - this.props.goTo('/tag/create'); - }} - > - - - ); - - public render() { - return ( - - - - {(autocompleteProps) => ( -
{ - this.props.setUrlState({ tagsKBar: value }); - this.props.containers.tags.reload(value); - }, - onSubmit: () => null, // todo - value: this.props.urlState.tagsKBar || '', - }} - actions={tagListActions} - actionHandler={this.handleTagsAction} - ref={this.state.tableRef} - items={this.props.containers.tags.state.list} - type={TagsTableType} - /> - )} - - - ); - } - - private handleTagsAction = async (action: AssignmentActionType) => { - const { intl } = this.props; - switch (action) { - case AssignmentActionType.Delete: - const success = await this.props.containers.tags.delete(this.getSelectedTags()); - if (!success) { - alert( - intl.formatMessage({ - id: 'xpack.beatsManagement.tags.someTagsMightBeAssignedToBeatsTitle', - defaultMessage: - 'Some of these tags might be assigned to beats. Please ensure tags being removed are not activly assigned', - }) - ); - } else { - if (this.state.tableRef && this.state.tableRef.current) { - this.state.tableRef.current.resetSelection(); - } - } - break; - } - }; - - private getSelectedTags = () => { - return this.state.tableRef.current ? this.state.tableRef.current.state.selection : []; - }; -} - -export const TagsPage = injectI18n(TagsPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx b/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx deleted file mode 100644 index 0ab02430e90e61..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/overview/enrolled_beats.tsx +++ /dev/null @@ -1,355 +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 { - EuiButton, - EuiButtonEmpty, - EuiGlobalToastList, - EuiModal, - EuiModalBody, - EuiModalHeader, - EuiModalHeaderTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { flatten, sortBy } from 'lodash'; -import moment from 'moment'; -import React from 'react'; -import { BeatTag, CMBeat } from '../../../common/domain_types'; -import { EnrollBeat } from '../../components/enroll_beats'; -import { Breadcrumb } from '../../components/navigation/breadcrumb'; -import { BeatsTableType, Table } from '../../components/table'; -import { beatsListActions } from '../../components/table/action_schema'; -import { AssignmentActionType } from '../../components/table/table'; -import { WithKueryAutocompletion } from '../../containers/with_kuery_autocompletion'; -import { AppPageProps } from '../../frontend_types'; - -interface PageProps extends AppPageProps { - renderAction: (area: () => JSX.Element) => void; - intl: InjectedIntl; -} - -interface PageState { - notifications: any[]; - tags: BeatTag[] | null; - beats: CMBeat[]; - assignmentOptions: BeatTag[] | null; -} - -class BeatsPageComponent extends React.PureComponent { - private tableRef: React.RefObject = React.createRef(); - constructor(props: PageProps) { - super(props); - - this.state = { - notifications: [], - tags: null, - beats: [], - assignmentOptions: null, - }; - - props.renderAction(this.renderActionArea); - } - - public componentDidMount() { - if (this.props.urlState.beatsKBar) { - this.props.containers.beats.reload(this.props.urlState.beatsKBar); - } - this.updateBeatsData(this.props.urlState.beatsKBar); - } - - public async updateBeatsData(beatsKBar?: string) { - const beats = sortBy(await this.props.libs.beats.getAll(beatsKBar), 'id') || []; - const tags = await this.props.libs.tags.getTagsWithIds(flatten(beats.map((beat) => beat.tags))); - - this.setState({ - tags, - beats, - }); - } - - public renderActionArea = () => ( - - { - // random, but specific number ensures new tab does not overwrite another _newtab in chrome - // and at the same time not truly random so that many clicks of the link open many tabs at this same URL - window.open( - 'https://www.elastic.co/guide/en/beats/libbeat/current/getting-started.html', - '_newtab35628937456' - ); - }} - > - - - { - this.props.goTo(`/overview/enrolled_beats/enroll`); - }} - > - - - - {this.props.location.pathname === '/overview/enrolled_beats/enroll' && ( - { - this.props.setUrlState({ - enrollmentToken: '', - }); - this.props.goTo(`/overview/enrolled_beats`); - }} - style={{ width: '640px' }} - > - - - - - - - { - const enrollmentTokens = await this.props.libs.tokens.createEnrollmentTokens(); - this.props.setUrlState({ - enrollmentToken: enrollmentTokens[0], - }); - }} - onBeatEnrolled={() => { - this.props.setUrlState({ - enrollmentToken: '', - }); - }} - /> - {!this.props.urlState.enrollmentToken && ( - - { - this.props.goTo('/overview/enrolled_beats'); - }} - > - Done - - - )} - - - )} - - ); - - public render() { - return ( - - - - {(autocompleteProps) => ( -
{ - this.props.setUrlState({ beatsKBar: value }); - - this.updateBeatsData(value); - }, - onSubmit: () => null, // todo - value: this.props.urlState.beatsKBar || '', - }} - actions={beatsListActions} - actionData={{ - tags: this.state.assignmentOptions, - }} - actionHandler={async (action: AssignmentActionType, payload: any) => { - switch (action) { - case AssignmentActionType.Assign: - const status = await this.props.containers.beats.toggleTagAssignment( - payload, - this.getSelectedBeats() - ); - await this.updateBeatsData(); - this.notifyUpdatedTagAssociation(status, this.getSelectedBeats(), payload); - break; - case AssignmentActionType.Delete: - await this.props.containers.beats.deactivate(this.getSelectedBeats()); - await this.updateBeatsData(); - this.notifyBeatDisenrolled(this.getSelectedBeats()); - break; - case AssignmentActionType.Reload: - const assignmentOptions = await this.props.libs.tags.getassignableTagsForBeats( - this.getSelectedBeats() - ); - this.setState({ assignmentOptions }); - break; - } - }} - items={this.state.beats.map((beat) => ({ - ...beat, - tags: (this.state.tags || []).filter((tag) => beat.tags.includes(tag.id)), - }))} - ref={this.tableRef} - type={BeatsTableType} - /> - )} - - this.setState({ notifications: [] })} - toastLifeTimeMs={5000} - /> - - ); - } - - private notifyBeatDisenrolled = async (beats: CMBeat[]) => { - const { intl } = this.props; - let title; - let text; - if (beats.length === 1) { - title = intl.formatMessage( - { - id: 'xpack.beatsManagement.beats.beatDisenrolledNotificationTitle', - defaultMessage: '{firstBeatNameOrId} disenrolled', - }, - { - firstBeatNameOrId: `"${beats[0].name || beats[0].id}"`, - } - ); - text = intl.formatMessage( - { - id: 'xpack.beatsManagement.beats.beatDisenrolledNotificationDescription', - defaultMessage: 'Beat with ID {firstBeatId} was disenrolled.', - }, - { - firstBeatId: `"${beats[0].id}"`, - } - ); - } else { - title = intl.formatMessage( - { - id: 'xpack.beatsManagement.beats.disenrolledBeatsNotificationTitle', - defaultMessage: '{beatsLength} beats disenrolled', - }, - { - beatsLength: beats.length, - } - ); - } - - this.setState({ - notifications: this.state.notifications.concat({ - color: 'warning', - id: `disenroll_${new Date()}`, - title, - text, - }), - }); - }; - - private notifyUpdatedTagAssociation = ( - action: 'added' | 'removed', - beats: CMBeat[], - tag: string - ) => { - const { intl } = this.props; - const notificationMessage = - action === 'removed' - ? intl.formatMessage( - { - id: 'xpack.beatsManagement.beats.removedNotificationDescription', - defaultMessage: - 'Removed tag {tag} from {assignmentsLength, plural, one {beat {beatName}} other {# beats}}.', - }, - { - tag: `"${tag}"`, - assignmentsLength: beats.length, - beatName: `"${beats[0].name || beats[0].id}"`, - } - ) - : intl.formatMessage( - { - id: 'xpack.beatsManagement.beats.addedNotificationDescription', - defaultMessage: - 'Added tag {tag} to {assignmentsLength, plural, one {beat {beatName}} other {# beats}}.', - }, - { - tag: `"${tag}"`, - assignmentsLength: beats.length, - beatName: `"${beats[0].name || beats[0].id}"`, - } - ); - const notificationTitle = - action === 'removed' - ? intl.formatMessage( - { - id: 'xpack.beatsManagement.beats.removedNotificationTitle', - defaultMessage: '{assignmentsLength, plural, one {Tag} other {Tags}} removed', - }, - { - assignmentsLength: beats.length, - } - ) - : intl.formatMessage( - { - id: 'xpack.beatsManagement.beats.addedNotificationTitle', - defaultMessage: '{assignmentsLength, plural, one {Tag} other {Tags}} added', - }, - { - assignmentsLength: beats.length, - } - ); - - this.setState({ - notifications: this.state.notifications.concat({ - color: 'success', - id: `tag-${moment.now()}`, - text:

{notificationMessage}

, - title: notificationTitle, - }), - }); - }; - - private getSelectedBeats = (): CMBeat[] => { - if (!this.tableRef.current) { - return []; - } - const selectedIds = this.tableRef.current.state.selection.map((beat: any) => beat.id); - const beats: CMBeat[] = []; - selectedIds.forEach((id: any) => { - const beat = this.props.containers.beats.state.list.find((b) => b.id === id); - if (beat) { - beats.push(beat); - } - }); - return beats; - }; -} - -export const BeatsPage = injectI18n(BeatsPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/overview/index.tsx b/x-pack/plugins/beats_management/public/pages/overview/index.tsx deleted file mode 100644 index 942f600b09ea8e..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/overview/index.tsx +++ /dev/null @@ -1,110 +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 { - EuiBetaBadge, - EuiFlexGroup, - EuiFlexItem, - EuiTab, - // @ts-ignore types for EuiTab not currently available - EuiTabs, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import { Subscribe } from 'unstated'; -import { CMBeat } from '../../../common/domain_types'; -import { PrimaryLayout } from '../../components/layouts/primary'; -import { ChildRoutes } from '../../components/navigation/child_routes'; -import { BeatsContainer } from '../../containers/beats'; -import { TagsContainer } from '../../containers/tags'; -import { withUrlState } from '../../containers/with_url_state'; -import { AppPageProps } from '../../frontend_types'; - -interface MainPagesState { - enrollBeat?: { - enrollmentToken: string; - } | null; - beats: CMBeat[]; - loadedBeatsAtLeastOnce: boolean; -} - -class MainPageComponent extends React.PureComponent { - constructor(props: AppPageProps) { - super(props); - this.state = { - loadedBeatsAtLeastOnce: false, - beats: [], - }; - } - public onTabClicked = (path: string) => { - return () => { - this.props.goTo(path); - }; - }; - - public render() { - return ( - - {'Beats'} - - - - - - } - hideBreadcrumbs={this.props.libs.framework.versionGreaterThen('6.7.0')} - > - {(renderAction: any) => ( - - {(beats: BeatsContainer, tags: TagsContainer) => ( - - - - - - - - - - - - )} - - )} - - ); - } -} - -export const MainPage = withUrlState(MainPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/tag/create.tsx b/x-pack/plugins/beats_management/public/pages/tag/create.tsx deleted file mode 100644 index 63651156760010..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/tag/create.tsx +++ /dev/null @@ -1,160 +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 { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import euiVars from '@elastic/eui/dist/eui_theme_light.json'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import 'brace/mode/yaml'; -import 'brace/theme/github'; -import { isEqual } from 'lodash'; -import React from 'react'; -import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants/configuration_blocks'; -import { BeatTag, ConfigurationBlock } from '../../../common/domain_types'; -import { PrimaryLayout } from '../../components/layouts/primary'; -import { TagEdit } from '../../components/tag'; -import { AppPageProps } from '../../frontend_types'; -import { randomEUIColor } from '../../utils/random_eui_color'; - -interface TagPageState { - showFlyout: boolean; - tag: BeatTag; - configuration_blocks: ConfigurationBlock[]; - currentConfigPage: number; -} -class TagCreatePageComponent extends React.PureComponent< - AppPageProps & { - intl: InjectedIntl; - }, - TagPageState -> { - constructor(props: AppPageProps & { intl: InjectedIntl }) { - super(props); - - this.state = { - showFlyout: false, - currentConfigPage: 0, - tag: { - id: '', - name: '', - color: randomEUIColor(euiVars), - hasConfigurationBlocksTypes: [], - }, - configuration_blocks: [], - }; - } - public render() { - const { intl } = this.props; - const blockStartingIndex = this.state.currentConfigPage * 5; - return ( - -
- - this.setState((oldState) => ({ - tag: { ...oldState.tag, [field]: value }, - })) - } - onConfigListChange={(index: number) => { - this.setState({ - currentConfigPage: index, - }); - }} - onConfigAddOrEdit={(block: ConfigurationBlock) => { - this.setState((previousState) => ({ - configuration_blocks: previousState.configuration_blocks.concat([block]), - })); - }} - onConfigRemoved={(block: ConfigurationBlock) => { - this.setState((previousState) => { - const selectedIndex = previousState.configuration_blocks.findIndex((c) => { - return isEqual(block, c); - }); - const blocks = [...previousState.configuration_blocks]; - blocks.splice(selectedIndex, 1); - return { - configuration_blocks: blocks, - }; - }); - }} - /> - - - - 1 // || this.state.tag.configuration_blocks.length === 0 - } - onClick={this.saveTag} - > - - - - - this.props.goTo('/overview/configuration_tags')}> - - - - -
-
- ); - } - - private saveTag = async () => { - const newTag = await this.props.containers.tags.upsertTag(this.state.tag); - if (!newTag) { - return alert( - i18n.translate('xpack.beatsManagement.createTag.errorSavingTagTitle', { - defaultMessage: 'error saving tag', - }) - ); - } - const createBlocksResponse = await this.props.libs.configBlocks.upsert( - this.state.configuration_blocks.map((block) => ({ ...block, tag: this.state.tag.id })) - ); - const creationError = createBlocksResponse.results.reduce( - (err: string, resp) => (!err ? (err = resp.error ? resp.error.message : '') : err), - '' - ); - if (creationError) { - return alert(creationError); - } - - this.props.goTo(`/overview/configuration_tags`); - }; - private getNumExclusiveConfigurationBlocks = () => - this.state.configuration_blocks - .map(({ type }) => UNIQUENESS_ENFORCING_TYPES.some((uniqueType) => uniqueType === type)) - .reduce((acc, cur) => (cur ? acc + 1 : acc), 0); -} - -export const TagCreatePage = injectI18n(TagCreatePageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/tag/edit.tsx b/x-pack/plugins/beats_management/public/pages/tag/edit.tsx deleted file mode 100644 index 7c023a14b16bd6..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/tag/edit.tsx +++ /dev/null @@ -1,203 +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 { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import 'brace/mode/yaml'; -import 'brace/theme/github'; -import { flatten } from 'lodash'; -import React from 'react'; -import { UNIQUENESS_ENFORCING_TYPES } from '../../../common/constants'; -import { BeatTag, CMBeat, ConfigurationBlock } from '../../../common/domain_types'; -import { PrimaryLayout } from '../../components/layouts/primary'; -import { TagEdit } from '../../components/tag'; -import { AppPageProps } from '../../frontend_types'; -interface TagPageState { - showFlyout: boolean; - attachedBeats: CMBeat[] | null; - tag: BeatTag; - beatsTags: BeatTag[]; - configuration_blocks: { - error?: string | undefined; - list: ConfigurationBlock[]; - page: number; - total: number; - }; -} -class TagEditPageComponent extends React.PureComponent< - AppPageProps & { - intl: InjectedIntl; - }, - TagPageState -> { - constructor(props: AppPageProps & { intl: InjectedIntl }) { - super(props); - - this.state = { - showFlyout: false, - attachedBeats: null, - beatsTags: [], - tag: { - id: props.match.params.tagid, - name: '', - color: '#fff', - hasConfigurationBlocksTypes: [], - }, - configuration_blocks: { - list: [], - page: 0, - total: 0, - }, - }; - } - - public UNSAFE_componentWillMount() { - this.loadTag(); - this.loadAttachedBeats(); - this.loadConfigBlocks(); - } - public render() { - const { intl } = this.props; - - return ( - -
- { - await this.props.containers.beats.removeTagsFromBeats(beatIds, this.state.tag.id); - await this.loadAttachedBeats(); - }} - onTagChange={(field: string, value: string | number) => - this.setState((oldState) => ({ - tag: { ...oldState.tag, [field]: value }, - })) - } - attachedBeats={ - (this.state.attachedBeats || []).map((beat) => ({ - ...beat, - tags: flatten( - beat.tags.map((tagId) => this.state.beatsTags.filter((tag) => tag.id === tagId)) - ), - })) as any - } - onConfigListChange={(index: number) => { - this.loadConfigBlocks(index); - }} - onConfigAddOrEdit={(block: ConfigurationBlock) => { - this.props.libs.configBlocks - .upsert([{ ...block, tag: this.state.tag.id }]) - .catch((e: any) => { - // eslint-disable-next-line - console.error('Error upseting config block', e); - }) - .then(() => { - this.loadConfigBlocks(this.state.configuration_blocks.page); - }); - }} - onConfigRemoved={(block: ConfigurationBlock) => { - this.props.libs.configBlocks - .delete(block.id) - .catch((e: any) => { - alert( - 'Error removing block, please check your browsers console logs for more details' - ); - // eslint-disable-next-line - console.error(`Error removing block ${block.id}`, e); - }) - .then(() => { - this.loadConfigBlocks(this.state.configuration_blocks.page); - }); - }} - /> - - - - 1 // || this.state.tag.configuration_blocks.length === 0 - } - onClick={this.saveTag} - > - - - - - this.props.goTo('/overview/configuration_tags')}> - - - - -
-
- ); - } - private loadConfigBlocks = async (page: number = -1) => { - const blocksResponse = await this.props.libs.configBlocks.getForTags([this.state.tag.id], page); - - this.setState({ - configuration_blocks: blocksResponse as { - error?: string | undefined; - list: ConfigurationBlock[]; - page: number; - total: number; - }, - }); - }; - - private loadTag = async () => { - const tags = await this.props.libs.tags.getTagsWithIds([this.props.match.params.tagid]); - if (tags.length === 0) { - // TODO do something to error https://github.com/elastic/kibana/issues/26023 - } - this.setState({ - tag: tags[0], - }); - }; - - private loadAttachedBeats = async () => { - const beats = await this.props.libs.beats.getBeatsWithTag(this.props.match.params.tagid); - const beatsTags = await this.props.libs.tags.getTagsWithIds( - flatten(beats.map((beat) => beat.tags)) - ); - this.setState({ - attachedBeats: beats, - beatsTags, - }); - }; - private saveTag = async () => { - await this.props.containers.tags.upsertTag(this.state.tag); - this.props.goTo(`/overview/configuration_tags`); - }; - private getNumExclusiveConfigurationBlocks = () => - this.state.configuration_blocks.list - .map(({ type }) => UNIQUENESS_ENFORCING_TYPES.some((uniqueType) => uniqueType === type)) - .reduce((acc, cur) => (cur ? acc + 1 : acc), 0); -} - -export const TagEditPage = injectI18n(TagEditPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/beat.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/beat.tsx deleted file mode 100644 index 2881bcfb18b565..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/beat.tsx +++ /dev/null @@ -1,65 +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 { EuiButton } from '@elastic/eui'; -import React, { Component } from 'react'; -import { EnrollBeat } from '../../../components/enroll_beats'; -import { AppPageProps } from '../../../frontend_types'; - -interface ComponentState { - readyToContinue: boolean; -} - -export class BeatsInitialEnrollmentPage extends Component { - constructor(props: AppPageProps) { - super(props); - this.state = { - readyToContinue: false, - }; - } - - public onBeatEnrolled = () => { - this.setState({ - readyToContinue: true, - }); - }; - - public createEnrollmentToken = async () => { - const enrollmentToken = await this.props.libs.tokens.createEnrollmentTokens(); - this.props.setUrlState({ - enrollmentToken: enrollmentToken[0], - }); - }; - - public render() { - return ( - - - {this.state.readyToContinue && ( - - { - this.props.goTo('/walkthrough/initial/tag'); - }} - > - Continue - - - )} - - ); - } -} diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx deleted file mode 100644 index 7a1bc5e2184fe3..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/finish.tsx +++ /dev/null @@ -1,136 +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 { EuiButton, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiPageContent } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React from 'react'; -import { CMBeat } from '../../../../common/domain_types'; -import { AppPageProps } from '../../../frontend_types'; - -interface PageState { - assigned: boolean; -} -class FinishWalkthrough extends React.Component< - AppPageProps & { - intl: InjectedIntl; - }, - PageState -> { - constructor( - props: AppPageProps & { - intl: InjectedIntl; - } - ) { - super(props); - - this.state = { - assigned: false, - }; - } - - public componentDidMount() { - setTimeout(async () => { - const done = await this.assignTagToBeat(); - - if (done) { - this.setState({ - assigned: true, - }); - } - }, 300); - } - - public render() { - const { goTo } = this.props; - - return ( - - - - - - - } - body={ - -

- -

-
- } - actions={ - { - goTo('/overview/enrolled_beats'); - }} - > - - - } - /> -
-
-
- ); - } - - private assignTagToBeat = async () => { - const { intl } = this.props; - if (!this.props.urlState.enrollmentToken) { - return alert( - intl.formatMessage({ - id: 'xpack.beatsManagement.enrollBeat.assignTagToBeatInvalidURLNoTokenFountTitle', - defaultMessage: 'Invalid URL, no enrollmentToken found', - }) - ); - } - if (!this.props.urlState.createdTag) { - return alert( - intl.formatMessage({ - id: 'xpack.beatsManagement.enrollBeat.assignTagToBeatInvalidURLNoTagFoundTitle', - defaultMessage: 'Invalid URL, no createdTag found', - }) - ); - } - - const beat = await this.props.libs.beats.getBeatWithToken(this.props.urlState.enrollmentToken); - if (!beat) { - return alert( - intl.formatMessage({ - id: 'xpack.beatsManagement.enrollBeat.assignTagToBeatNotEnrolledProperlyTitle', - defaultMessage: 'Error: Beat not enrolled properly', - }) - ); - } - - await this.props.containers.beats.assignTagsToBeats( - [beat as CMBeat], - this.props.urlState.createdTag - ); - this.props.setUrlState({ - createdTag: '', - enrollmentToken: '', - }); - return true; - }; -} - -export const FinishWalkthroughPage = injectI18n(FinishWalkthrough); diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/index.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/index.tsx deleted file mode 100644 index 506aa87bc51ef2..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/index.tsx +++ /dev/null @@ -1,97 +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 { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React from 'react'; -import { NoDataLayout } from '../../../components/layouts/no_data'; -import { WalkthroughLayout } from '../../../components/layouts/walkthrough'; -import { ChildRoutes } from '../../../components/navigation/child_routes'; -import { ConnectedLink } from '../../../components/navigation/connected_link'; -import { AppPageProps } from '../../../frontend_types'; - -type Props = AppPageProps & { - intl: InjectedIntl; -}; - -const InitialWalkthroughPageComponent: React.FC = (props) => { - if (props.location.pathname === '/walkthrough/initial') { - return ( - - {'Beats central management '} - - - - - } - actionSection={ - - - {' '} - - - } - > -

- -

-
- ); - } - return ( - { - // FIXME implament goto - }} - activePath={props.location.pathname} - > - - - ); -}; - -export const InitialWalkthroughPage = injectI18n(InitialWalkthroughPageComponent); diff --git a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx b/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx deleted file mode 100644 index eb1e251b2896d3..00000000000000 --- a/x-pack/plugins/beats_management/public/pages/walkthrough/initial/tag.tsx +++ /dev/null @@ -1,137 +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 { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { isEqual } from 'lodash'; -import React, { Component } from 'react'; -import uuidv4 from 'uuid/v4'; -import { BeatTag, ConfigurationBlock } from '../../../../common/domain_types'; -import { TagEdit } from '../../../components/tag/tag_edit'; -import { AppPageProps } from '../../../frontend_types'; -interface PageState { - tag: BeatTag; - configuration_blocks: ConfigurationBlock[]; - currentConfigPage: number; -} - -export class InitialTagPage extends Component { - constructor(props: AppPageProps) { - super(props); - this.state = { - tag: { - id: uuidv4(), - name: '', - color: '#DD0A73', - hasConfigurationBlocksTypes: [], - }, - configuration_blocks: [], - currentConfigPage: 0, - }; - - if (props.urlState.createdTag) { - this.loadTag(); - } - } - - public render() { - const blockStartingIndex = this.state.currentConfigPage * 5; - - return ( - - - this.setState((oldState) => ({ - tag: { ...oldState.tag, [field]: value }, - })) - } - onConfigListChange={(index: number) => { - this.setState({ - currentConfigPage: index, - }); - }} - onConfigAddOrEdit={(block: ConfigurationBlock) => { - this.setState((previousState) => ({ - configuration_blocks: previousState.configuration_blocks.concat([block]), - })); - }} - onConfigRemoved={(block: ConfigurationBlock) => { - this.setState((previousState) => { - const selectedIndex = previousState.configuration_blocks.findIndex((c) => { - return isEqual(block, c); - }); - const blocks = [...previousState.configuration_blocks]; - blocks.splice(selectedIndex, 1); - return { - configuration_blocks: blocks, - }; - }); - }} - /> - - - - - - - - - ); - } - - private loadTag = async () => { - const tags = await this.props.libs.tags.getTagsWithIds([this.state.tag.id]); - if (tags.length > 0) { - this.setState({ - tag: tags[0], - }); - } - }; - - private saveTag = async () => { - const newTag = await this.props.libs.tags.upsertTag(this.state.tag as BeatTag); - if (!newTag) { - return alert( - i18n.translate('xpack.beatsManagement.createTag.errorSavingTagTitle', { - defaultMessage: 'error saving tag', - }) - ); - } - const createBlocksResponse = await this.props.libs.configBlocks.upsert( - this.state.configuration_blocks.map((block) => ({ ...block, tag: this.state.tag.id })) - ); - const creationError = createBlocksResponse.results.reduce( - (err: string, resp) => (!err ? (err = resp.error ? resp.error.message : '') : err), - '' - ); - if (creationError) { - return alert(creationError); - } - this.props.setUrlState({ - createdTag: newTag.id, - }); - this.props.goTo(`/walkthrough/initial/finish`); - }; -} diff --git a/x-pack/plugins/beats_management/public/router.tsx b/x-pack/plugins/beats_management/public/router.tsx deleted file mode 100644 index 2c328c2745c14d..00000000000000 --- a/x-pack/plugins/beats_management/public/router.tsx +++ /dev/null @@ -1,132 +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 { get } from 'lodash'; -import React, { Component } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import { Loading } from './components/loading'; -import { ChildRoutes } from './components/navigation/child_routes'; -import { BeatsContainer } from './containers/beats'; -import { TagsContainer } from './containers/tags'; -import { URLStateProps, WithURLState } from './containers/with_url_state'; -import { FrontendLibs } from './lib/types'; -import { routeMap } from './pages/index'; - -interface RouterProps { - libs: FrontendLibs; - tagsContainer: TagsContainer; - beatsContainer: BeatsContainer; -} -interface RouterState { - loading: boolean; -} - -export class AppRouter extends Component { - constructor(props: RouterProps) { - super(props); - this.state = { - loading: true, - }; - } - - public async UNSAFE_componentWillMount() { - if (this.state.loading === true) { - try { - await this.props.beatsContainer.reload(); - await this.props.tagsContainer.reload(); - } catch (e) { - // TODO in a furture version we will better manage this "error" in a returned arg - } - - this.setState({ - loading: false, - }); - } - } - - public render() { - if (this.state.loading === true) { - return ; - } - - const countOfEverything = - this.props.beatsContainer.state.list.length + this.props.tagsContainer.state.list.length; - - return ( - - {/* Redirects mapping */} - - {/* License check (UI displays when license exists but is expired) */} - {get(this.props.libs.framework.info, 'license.expired', true) && ( - - !props.location.pathname.includes('/error') ? ( - - ) : null - } - /> - )} - - {/* Ensure security is eanabled for elastic and kibana */} - {!get(this.props.libs.framework.info, 'security.enabled', true) && ( - - !props.location.pathname.includes('/error') ? ( - - ) : null - } - /> - )} - - {/* Make sure the user has correct permissions */} - {!this.props.libs.framework.currentUserHasOneOfRoles( - ['beats_admin'].concat(this.props.libs.framework.info.settings.defaultUserRoles) - ) && ( - - !props.location.pathname.includes('/error') ? ( - - ) : null - } - /> - )} - - {/* If there are no beats or tags yet, redirect to the walkthrough */} - {countOfEverything === 0 && ( - - !props.location.pathname.includes('/walkthrough') ? ( - - ) : null - } - /> - )} - - {/* This app does not make use of a homepage. The mainpage is overview/enrolled_beats */} - } /> - - - {/* Render routes from the FS */} - - {(URLProps: URLStateProps) => ( - - )} - - - ); - } -} diff --git a/x-pack/plugins/beats_management/public/utils/random_eui_color.ts b/x-pack/plugins/beats_management/public/utils/random_eui_color.ts deleted file mode 100644 index 52d847cc6a5a0e..00000000000000 --- a/x-pack/plugins/beats_management/public/utils/random_eui_color.ts +++ /dev/null @@ -1,26 +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 { sample } from 'lodash'; - -export const randomEUIColor = (euiVars: any) => { - const rgb = sample( - Object.keys(euiVars) - .filter((key) => key.startsWith('euiColorVis')) - .map((key) => (euiVars as any)[key]) - ); - - const matchedrgb = rgb.match( - /^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i - ); - return matchedrgb && matchedrgb.length === 4 - ? '#' + - ('0' + parseInt(matchedrgb[1], 10).toString(16)).slice(-2) + - ('0' + parseInt(matchedrgb[2], 10).toString(16)).slice(-2) + - ('0' + parseInt(matchedrgb[3], 10).toString(16)).slice(-2) - : ''; -}; diff --git a/x-pack/plugins/beats_management/public/utils/typed_react.ts b/x-pack/plugins/beats_management/public/utils/typed_react.ts deleted file mode 100644 index ee5ffc2969bb07..00000000000000 --- a/x-pack/plugins/beats_management/public/utils/typed_react.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -export type RendererResult = React.ReactElement | null; -export type RendererFunction = (args: RenderArgs) => Result; - -export type StateUpdater = ( - prevState: Readonly, - prevProps: Readonly -) => State | null; - -export function composeStateUpdaters(...updaters: Array>) { - return (state: State, props: Props) => - updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); -} diff --git a/x-pack/plugins/beats_management/readme.md b/x-pack/plugins/beats_management/readme.md deleted file mode 100644 index 75adf428772e43..00000000000000 --- a/x-pack/plugins/beats_management/readme.md +++ /dev/null @@ -1,39 +0,0 @@ -# Beats CM - -Notes: -Failure to have auth enabled in Kibana will make for a broken UI. UI-based errors not yet in place - -## Testing - -### Unit tests - -Run Jest tests: - -Documentation: https://www.elastic.co/guide/en/kibana/current/development-tests.html#_unit_testing - -``` -yarn test:jest x-pack/plugins/beats --watch -``` - -### API tests - -In one shell, from **~/kibana/x-pack**: -`node scripts/functional_tests-server.js` - -In another shell, from **~kibana/x-pack**: -`node ../scripts/functional_test_runner.js --config test/api_integration/config.ts`. - -### Manual e2e testing - -- Run this command to fake an enrolling beat (from beats_management dir) - -``` -node scripts/enroll.js -``` - -- Run a command to setup a fake large-scale deployment - Note: `ts-node` is required to be installed gloably from NPM/Yarn for this action - -``` -ts-node scripts/fake_env.ts <# of beats> <# of tags per beat> <# of congifs per tag> -``` diff --git a/x-pack/plugins/beats_management/scripts/enroll.js b/x-pack/plugins/beats_management/scripts/enroll.js deleted file mode 100644 index 3c6712ada8e9b2..00000000000000 --- a/x-pack/plugins/beats_management/scripts/enroll.js +++ /dev/null @@ -1,42 +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. - */ - -const request = require('request'); -const Chance = require('chance'); // eslint-disable-line -const args = process.argv.slice(2); -const chance = new Chance(); - -const enroll = async (kibanaURL, token) => { - const beatId = chance.word(); - - if (!token) { - token = kibanaURL; - kibanaURL = 'http://localhost:5601'; - } - - await request( - { - url: `${kibanaURL}/api/beats/agent/${beatId}`, - method: 'POST', - headers: { - 'kbn-xsrf': 'xxx', - 'kbn-beats-enrollment-token': token, - }, - body: JSON.stringify({ - type: 'filebeat', - host_name: `${chance.word()}.bar.com`, - name: chance.word(), - version: '6.3.0', - }), - }, - (error, response, body) => { - console.log(error, body); - } - ); -}; - -enroll(...args); diff --git a/x-pack/plugins/beats_management/scripts/fake_env.ts b/x-pack/plugins/beats_management/scripts/fake_env.ts deleted file mode 100644 index e45bbe2277387b..00000000000000 --- a/x-pack/plugins/beats_management/scripts/fake_env.ts +++ /dev/null @@ -1,158 +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 Chance from 'chance'; -// @ts-ignore -import request from 'request'; -import uuidv4 from 'uuid/v4'; -import { configBlockSchemas } from '../common/config_schemas'; -import { BeatTag } from '../common/domain_types'; -import { compose } from '../public/lib/compose/scripts'; -const args = process.argv.slice(2); -const chance = new Chance(); - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -function getRandomColor() { - const letters = '0123456789ABCDEF'; - let color = '#'; - for (let i = 0; i < 6; i++) { - color += letters[Math.floor(Math.random() * 16)]; - } - return color; -} - -const enroll = async (kibanaURL: string, token: string) => { - const beatId = uuidv4(); - - await request( - { - url: `${kibanaURL}/api/beats/agent/${beatId}`, - method: 'POST', - headers: { - 'kbn-xsrf': 'xxx', - 'kbn-beats-enrollment-token': token, - }, - body: JSON.stringify({ - type: Math.random() >= 0.5 ? 'filebeat' : 'metricbeat', - host_name: `${chance.word()}.local`, - name: chance.word(), - version: '6.7.0', - }), - }, - (error: any, response: any, body: any) => { - const res = JSON.parse(body); - if (res.message) { - // eslint-disable-next-line - console.log(res.message); - } - } - ); -}; - -const start = async ( - kibanaURL: string, - numberOfBeats = 10, - maxNumberOfTagsPerBeat = 2, - maxNumberOfConfigsPerTag = 4 -) => { - try { - const libs = compose(kibanaURL); - // eslint-disable-next-line - console.error(`Enrolling ${numberOfBeats} fake beats...`); - - const enrollmentTokens = await libs.tokens.createEnrollmentTokens(numberOfBeats); - process.stdout.write(`enrolling fake beats... 0 of ${numberOfBeats}`); - let count = 0; - for (const token of enrollmentTokens) { - count++; - // @ts-ignore - process.stdout.clearLine(); - // @ts-ignore - process.stdout.cursorTo(0); - process.stdout.write(`enrolling fake beats... ${count} of ${numberOfBeats}`); - - await enroll(kibanaURL, token); - await sleep(10); - } - process.stdout.write('\n'); - - await sleep(2000); - // eslint-disable-next-line - console.error(`${numberOfBeats} fake beats are enrolled`); - const beats = await libs.beats.getAll(); - - // eslint-disable-next-line - console.error(`Creating tags, configs, and assigning them...`); - process.stdout.write(`creating tags/configs for beat... 0 of ${numberOfBeats}`); - count = 0; - for (const beat of beats) { - count++; - // @ts-ignore - process.stdout.clearLine(); - // @ts-ignore - process.stdout.cursorTo(0); - process.stdout.write(`creating tags w/configs for beat... ${count} of ${numberOfBeats}`); - - const tags = await Promise.all( - [...Array(maxNumberOfTagsPerBeat)].map(() => { - return libs.tags.upsertTag({ - name: chance.word(), - color: getRandomColor(), - hasConfigurationBlocksTypes: [] as string[], - } as BeatTag); - }) - ); - await libs.beats.assignTagsToBeats( - tags.map((tag: any) => ({ - beatId: beat.id, - tag: tag.id, - })) - ); - - await Promise.all( - tags.map((tag: any) => { - return libs.configBlocks.upsert( - [...Array(maxNumberOfConfigsPerTag)].map( - () => - ({ - type: configBlockSchemas[Math.floor(Math.random())].id, - description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sint ista Graecorum; -Nihil ad rem! Ne sit sane; Quod quidem nobis non saepe contingit. -Duo Reges: constructio interrete. Itaque his sapiens semper vacabit.`.substring( - 0, - Math.floor(Math.random() * (0 - 115 + 1)) - ), - tag: tag.id, - last_updated: new Date(), - config: {}, - } as any) - ) - ); - }) - ); - } - } catch (e) { - if (e.response && e.response.data && e.response.message) { - // eslint-disable-next-line - console.error(e.response.data.message); - } else if (e.response && e.response.data && e.response.reason) { - // eslint-disable-next-line - console.error(e.response.data.reason); - } else if (e.code) { - // eslint-disable-next-line - console.error(e.code); - } else { - // eslint-disable-next-line - console.error(e); - } - } -}; - -// @ts-ignore -start(...args); diff --git a/x-pack/plugins/beats_management/server/index.ts b/x-pack/plugins/beats_management/server/index.ts deleted file mode 100644 index 5489fce1f36a3c..00000000000000 --- a/x-pack/plugins/beats_management/server/index.ts +++ /dev/null @@ -1,22 +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 { PluginInitializer } from '../../../../src/core/server'; -import { beatsManagementConfigSchema } from '../common'; -import { BeatsManagementPlugin } from './plugin'; - -export const config = { - schema: beatsManagementConfigSchema, - - exposeToBrowser: { - defaultUserRoles: true, - encryptionKey: true, - enrollmentTokensTtlInSeconds: true, - }, -}; - -export const plugin: PluginInitializer<{}, {}> = (context) => new BeatsManagementPlugin(context); diff --git a/x-pack/plugins/beats_management/server/index_templates/beats_template.json b/x-pack/plugins/beats_management/server/index_templates/beats_template.json deleted file mode 100644 index 36fc753c68745f..00000000000000 --- a/x-pack/plugins/beats_management/server/index_templates/beats_template.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "index_patterns": [".management-beats"], - "version": 70000, - "settings": { - "index": { - "number_of_shards": 1, - "auto_expand_replicas": "0-1", - "codec": "best_compression" - } - }, - "mappings": { - "dynamic": "strict", - "properties": { - "type": { - "type": "keyword" - }, - "configuration_block": { - "properties": { - "id": { - "type": "keyword" - }, - "type": { - "type": "keyword" - }, - "tag": { - "type": "keyword" - }, - "description": { - "type": "text" - }, - "config": { - "type": "keyword" - }, - "last_updated": { - "type": "date" - } - } - }, - "enrollment_token": { - "properties": { - "token": { - "type": "keyword" - }, - "expires_on": { - "type": "date" - } - } - }, - "tag": { - "properties": { - "id": { - "type": "keyword" - }, - "name": { - "type": "keyword" - }, - "color": { - "type": "keyword" - }, - "hasConfigurationBlocksTypes": { - "type": "keyword" - } - } - }, - "beat": { - "properties": { - "id": { - "type": "keyword" - }, - "status": { - "properties": { - "type": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "event": { - "properties": { - "type": { - "type": "keyword" - }, - "message": { - "type": "text" - }, - "uuid": { - "type": "keyword" - } - } - } - } - }, - "active": { - "type": "boolean" - }, - "last_checkin": { - "type": "date" - }, - "enrollment_token": { - "type": "keyword" - }, - "access_token": { - "type": "keyword" - }, - "verified_on": { - "type": "date" - }, - "type": { - "type": "keyword" - }, - "version": { - "type": "keyword" - }, - "host_ip": { - "type": "ip" - }, - "host_name": { - "type": "keyword" - }, - "ephemeral_id": { - "type": "keyword" - }, - "tags": { - "type": "keyword" - }, - "metadata": { - "dynamic": "true", - "type": "object" - }, - "name": { - "type": "keyword" - } - } - } - } - } -} diff --git a/x-pack/plugins/beats_management/server/index_templates/events_template.json b/x-pack/plugins/beats_management/server/index_templates/events_template.json deleted file mode 100644 index 4f8063f0481329..00000000000000 --- a/x-pack/plugins/beats_management/server/index_templates/events_template.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "index_patterns": [".management-beats-events-*"], - "version": 67000, - "index": { - "lifecycle": { - "name": ".beats-management-events-retention" - } - }, - "settings": { - "index": { - "number_of_shards": 1, - "auto_expand_replicas": "0-1", - "codec": "best_compression" - } - }, - "mappings": { - "_doc": { - "dynamic": "strict", - "properties": { - "type": { - "type": "keyword" - }, - "beat": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "event": { - "properties": { - "type": { - "type": "keyword" - }, - "message": { - "type": "text" - }, - "uuid": { - "type": "keyword" - } - } - } - } - } - } -} diff --git a/x-pack/plugins/beats_management/server/index_templates/index.ts b/x-pack/plugins/beats_management/server/index_templates/index.ts deleted file mode 100644 index ade9a86114e630..00000000000000 --- a/x-pack/plugins/beats_management/server/index_templates/index.ts +++ /dev/null @@ -1,11 +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 beatsIndexTemplate from './beats_template.json'; -import eventsIndexTemplate from './events_template.json'; - -export { beatsIndexTemplate, eventsIndexTemplate }; diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.ts deleted file mode 100644 index 995fec9825db88..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/adapter_types.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 { CMBeat } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; - -export interface CMBeatsAdapter { - insert(user: FrameworkUser, beat: CMBeat): Promise; - update(user: FrameworkUser, beat: CMBeat): Promise; - get(user: FrameworkUser, id: string): Promise; - getAll(user: FrameworkUser, ESQuery?: any): Promise; - getWithIds(user: FrameworkUser, beatIds: string[]): Promise; - getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise; - getBeatWithToken(user: FrameworkUser, enrollmentToken: string): Promise; - removeTagsFromBeats( - user: FrameworkUser, - removals: BeatsTagAssignment[] - ): Promise; - assignTagsToBeats( - user: FrameworkUser, - assignments: BeatsTagAssignment[] - ): Promise; -} - -export interface BeatsTagAssignment { - beatId: string; - tag: string; - idxInRequest?: number; -} - -interface BeatsReturnedTagAssignment { - status: number | null; - result?: string; -} - -export interface CMAssignmentReturn { - assignments: BeatsReturnedTagAssignment[]; -} - -export interface BeatsRemovalReturn { - removals: BeatsReturnedTagAssignment[]; -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts deleted file mode 100644 index 3fdba8f7441d19..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/beats/elasticsearch_beats_adapter.ts +++ /dev/null @@ -1,245 +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 { flatten, get as _get, omit } from 'lodash'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { CMBeat } from '../../../../common/domain_types'; -import { DatabaseAdapter } from '../database/adapter_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { BeatsTagAssignment, CMBeatsAdapter } from './adapter_types'; - -function formatWithTags(beat: CMBeat) { - const { tags, ...rest } = beat; - - return { - tags: tags || [], - ...rest, - }; -} - -export class ElasticsearchBeatsAdapter implements CMBeatsAdapter { - private database: DatabaseAdapter; - - constructor(database: DatabaseAdapter) { - this.database = database; - } - - public async get(user: FrameworkUser, id: string) { - const params = { - id: `beat:${id}`, - ignore: [404], - index: INDEX_NAMES.BEATS, - }; - - const response = await this.database.get(user, params); - if (!response.found) { - return null; - } - const beat = _get(response, '_source.beat') as CMBeat; - beat.tags = beat.tags || []; - return beat; - } - - public async insert(user: FrameworkUser, beat: CMBeat) { - const body = { - beat, - type: 'beat', - }; - - await this.database.index(user, { - body, - id: `beat:${beat.id}`, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }); - } - - public async update(user: FrameworkUser, beat: CMBeat) { - const body = { - beat, - type: 'beat', - }; - - const params = { - body, - id: `beat:${beat.id}`, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }; - await this.database.index(user, params); - } - - public async getWithIds(user: FrameworkUser, beatIds: string[]) { - const ids = beatIds.map((beatId) => `beat:${beatId}`); - - const params = { - body: { - ids, - }, - index: INDEX_NAMES.BEATS, - }; - const response = await this.database.mget(user, params); - - return _get(response, 'docs', []) - .filter((b: any) => b.found) - .map((b: any) => ({ tags: [], ...b._source.beat })); - } - - public async getAllWithTags(user: FrameworkUser, tagIds: string[]): Promise { - const params = { - ignore: [404], - index: INDEX_NAMES.BEATS, - body: { - query: { - terms: { 'beat.tags': tagIds }, - }, - }, - }; - - const response = await this.database.search(user, params); - - const beats = _get(response, 'hits.hits', []) as CMBeat[]; - - if (beats.length === 0) { - return []; - } - return beats.map((beat: any) => - formatWithTags(omit(beat._source.beat as CMBeat, ['access_token']) as CMBeat) - ); - } - - public async getBeatWithToken( - user: FrameworkUser, - enrollmentToken: string - ): Promise { - const params = { - ignore: [404], - index: INDEX_NAMES.BEATS, - body: { - query: { - match: { 'beat.enrollment_token': enrollmentToken }, - }, - }, - }; - - const response = await this.database.search(user, params); - - const beats = _get(response, 'hits.hits', []) as CMBeat[]; - - if (beats.length === 0) { - return null; - } - return omit(_get(formatWithTags(beats[0]), '_source.beat'), ['access_token']) as CMBeat; - } - - public async getAll(user: FrameworkUser, ESQuery?: any) { - const params = { - index: INDEX_NAMES.BEATS, - size: 10000, - ignore: [404], - body: { - query: { - bool: { - must: { - term: { - type: 'beat', - }, - }, - }, - }, - }, - }; - - if (ESQuery) { - params.body.query = { - ...params.body.query, - ...ESQuery, - }; - } - - let response; - try { - response = await this.database.search(user, params); - } catch (e) { - // TODO something - } - if (!response) { - return []; - } - const beats = _get(response, 'hits.hits', []) as any; - - return beats.map((beat: any) => - formatWithTags(omit(beat._source.beat as CMBeat, ['access_token']) as CMBeat) - ); - } - - public async removeTagsFromBeats( - user: FrameworkUser, - removals: BeatsTagAssignment[] - ): Promise { - const body = flatten( - removals.map(({ beatId, tag }) => { - const script = ` - def beat = ctx._source.beat; - if (beat.tags != null) { - beat.tags.removeAll([params.tag]); - }`; - - return [ - { update: { _id: `beat:${beatId}` } }, - { script: { source: script.replace(' ', ''), params: { tag } } }, - ]; - }) - ); - - const response = await this.database.bulk(user, { - body, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }); - return (_get(response, 'items', []) as any).map((item: any, resultIdx: number) => ({ - idxInRequest: removals[resultIdx].idxInRequest, - result: item.update.result, - status: item.update.status, - })); - } - - public async assignTagsToBeats( - user: FrameworkUser, - assignments: BeatsTagAssignment[] - ): Promise { - const body = flatten( - assignments.map(({ beatId, tag }) => { - const script = ` - def beat = ctx._source.beat; - if (beat.tags == null) { - beat.tags = []; - } - if (!beat.tags.contains(params.tag)) { - beat.tags.add(params.tag); - }`; - - return [ - { update: { _id: `beat:${beatId}` } }, - { script: { source: script.replace(' ', ''), params: { tag } } }, - ]; - }) - ); - - const response = await this.database.bulk(user, { - body, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }); - // console.log(response.items[0].update.error); - return (_get(response, 'items', []) as any).map((item: any, resultIdx: any) => ({ - idxInRequest: assignments[resultIdx].idxInRequest, - result: item.update.result, - status: item.update.status, - })); - } -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/adapter_types.ts deleted file mode 100644 index dcd2043bd0ea15..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/adapter_types.ts +++ /dev/null @@ -1,28 +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 { ConfigurationBlock } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; - -export interface ConfigurationBlockAdapter { - getByIds(user: FrameworkUser, ids: string[]): Promise; - getForTags( - user: FrameworkUser, - tagIds: string[], - page?: number, - size?: number - ): Promise<{ blocks: ConfigurationBlock[]; page: number; total: number }>; - delete( - user: FrameworkUser, - blockIds: string[] - ): Promise>; - create(user: FrameworkUser, configs: ConfigurationBlock[]): Promise; - deleteForTags( - user: FrameworkUser, - tagIds: string[] - ): Promise<{ success: boolean; reason?: string }>; -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts deleted file mode 100644 index 744aa0d87c7601..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/configuration_blocks/elasticsearch_configuration_block_adapter.ts +++ /dev/null @@ -1,168 +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 { flatten, get } from 'lodash'; -import uuidv4 from 'uuid/v4'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { ConfigurationBlock } from '../../../../common/domain_types'; -import { DatabaseAdapter } from '../database/adapter_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { ConfigurationBlockAdapter } from './adapter_types'; - -export class ElasticsearchConfigurationBlockAdapter implements ConfigurationBlockAdapter { - private database: DatabaseAdapter; - - constructor(database: DatabaseAdapter) { - this.database = database; - } - - public async getByIds(user: FrameworkUser, ids: string[]): Promise { - if (ids.length === 0) { - return []; - } - - const params = { - ignore: [404], - _source: true, - size: 10000, - index: INDEX_NAMES.BEATS, - body: { - ids: ids.map((id) => `configuration_block:${id}`), - }, - }; - - const response = await this.database.search(user, params); - const configs = get(response, 'hits.hits', []); - - return configs.map((tag: any) => ({ ...tag._source.tag, config: JSON.parse(tag._source.tag) })); - } - - public async getForTags( - user: FrameworkUser, - tagIds: string[], - page: number = 0, - size: number = 100 - ): Promise<{ blocks: ConfigurationBlock[]; page: number; total: number }> { - if (tagIds.length === 0) { - return { - page: 0, - total: 0, - blocks: [] as ConfigurationBlock[], - }; - } - - const params = { - ignore: [404], - index: INDEX_NAMES.BEATS, - body: { - from: page === -1 ? undefined : page * size, - size, - query: { - terms: { 'configuration_block.tag': tagIds }, - }, - }, - }; - let response; - if (page === -1) { - response = await this.database.searchAll(user, params); - } else { - response = await this.database.search(user, params); - } - const configs = get(response, 'hits.hits', []); - - return { - blocks: configs.map((block: any) => ({ - ...block._source.configuration_block, - config: JSON.parse(block._source.configuration_block.config || '{}'), - })), - page, - total: response.hits ? (response.hits.total as any).value : 0, - }; - } - - public async delete( - user: FrameworkUser, - ids: string[] - ): Promise> { - const result = await this.database.bulk(user, { - body: ids.map((id) => ({ delete: { _id: `configuration_block:${id}` } })), - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }); - - if (result.errors) { - if (result.items[0].result) { - throw new Error(result.items[0].result); - } - throw new Error((result.items[0] as any).index.error.reason); - } - - return result.items.map((item: any) => { - return { - id: item.delete._id, - success: item.delete.result === 'deleted', - reason: item.delete.result !== 'deleted' ? item.delete.result : undefined, - }; - }); - } - - public async deleteForTags( - user: FrameworkUser, - tagIds: string[] - ): Promise<{ success: boolean; reason?: string }> { - const result: any = await this.database.deleteByQuery(user, { - body: { - query: { - terms: { 'configuration_block.tag': tagIds }, - }, - }, - index: INDEX_NAMES.BEATS, - }); - - if (result.failures.length > 0) { - return { - success: false, - reason: result.failures[0], - }; - } - - return { - success: true, - }; - } - - public async create(user: FrameworkUser, configs: ConfigurationBlock[]): Promise { - const body = flatten( - configs.map((config) => { - const { id: configId, ...configWithoutId } = config; - const id = configId || uuidv4(); - return [ - { index: { _id: `configuration_block:${id}` } }, - { - type: 'configuration_block', - configuration_block: { id, ...configWithoutId, config: JSON.stringify(config.config) }, - }, - ]; - }) - ); - - const result = await this.database.bulk(user, { - body, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }); - - if (result.errors) { - if (result.items[0].result) { - throw new Error(result.items[0].result); - } - throw new Error((result.items[0] as any).index.error.reason); - } - - return result.items.map((item: any) => item.index._id); - } -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts deleted file mode 100644 index a1af8e79296355..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/database/adapter_types.ts +++ /dev/null @@ -1,302 +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 { FrameworkUser } from '../framework/adapter_types'; - -export interface DatabaseAdapter { - get( - user: FrameworkUser, - params: DatabaseGetParams - ): Promise>; - create( - user: FrameworkUser, - params: DatabaseCreateDocumentParams - ): Promise; - index( - user: FrameworkUser, - params: DatabaseIndexDocumentParams - ): Promise; - delete( - user: FrameworkUser, - params: DatabaseDeleteDocumentParams - ): Promise; - deleteByQuery( - user: FrameworkUser, - params: DatabaseSearchParams - ): Promise; - mget(user: FrameworkUser, params: DatabaseMGetParams): Promise>; - bulk( - user: FrameworkUser, - params: DatabaseBulkIndexDocumentsParams - ): Promise; - search(user: FrameworkUser, params: DatabaseSearchParams): Promise>; - searchAll( - user: FrameworkUser, - params: DatabaseSearchParams - ): Promise>; - putTemplate(name: string, template: any): Promise; -} - -export interface DatabaseSearchParams extends DatabaseGenericParams { - analyzer?: string; - analyzeWildcard?: boolean; - defaultOperator?: DefaultOperator; - df?: string; - explain?: boolean; - storedFields?: DatabaseNameList; - docvalueFields?: DatabaseNameList; - fielddataFields?: DatabaseNameList; - from?: number; - ignoreUnavailable?: boolean; - allowNoIndices?: boolean; - expandWildcards?: ExpandWildcards; - lenient?: boolean; - lowercaseExpandedTerms?: boolean; - preference?: string; - q?: string; - routing?: DatabaseNameList; - scroll?: string; - searchType?: 'query_then_fetch' | 'dfs_query_then_fetch'; - size?: number; - sort?: DatabaseNameList; - _source?: DatabaseNameList; - _sourceExclude?: DatabaseNameList; - _source_includes?: DatabaseNameList; - terminateAfter?: number; - stats?: DatabaseNameList; - suggestField?: string; - suggestMode?: 'missing' | 'popular' | 'always'; - suggestSize?: number; - suggestText?: string; - timeout?: string; - trackScores?: boolean; - version?: boolean; - requestCache?: boolean; - index?: DatabaseNameList; - type?: DatabaseNameList; -} - -export interface DatabaseSearchResponse { - took: number; - timed_out: boolean; - _scroll_id?: string; - _shards: DatabaseShardsResponse; - hits: { - total: number; - max_score: number; - hits: Array<{ - _index: string; - _id: string; - _score: number; - _source: T; - _seq_no?: number; - _primary_term?: number; - _explanation?: DatabaseExplanation; - fields?: any; - highlight?: any; - inner_hits?: any; - sort?: string[]; - }>; - }; - aggregations?: any; -} - -export interface DatabaseExplanation { - value: number; - description: string; - details: DatabaseExplanation[]; -} - -export interface DatabaseShardsResponse { - total: number; - successful: number; - failed: number; - skipped: number; -} - -export interface DatabaseGetDocumentResponse { - _index: string; - _id: string; - _seq_no: number; - _primary_term: number; - found: boolean; - _source: Source; -} - -export interface DatabaseBulkResponse { - took: number; - errors: boolean; - items: Array< - DatabaseDeleteDocumentResponse | DatabaseIndexDocumentResponse | DatabaseUpdateDocumentResponse - >; -} - -export interface DatabaseBulkIndexDocumentsParams extends DatabaseGenericParams { - waitForActiveShards?: string; - refresh?: DatabaseRefresh; - routing?: string; - timeout?: string; - fields?: DatabaseNameList; - _source?: DatabaseNameList; - _sourceExclude?: DatabaseNameList; - _source_includes?: DatabaseNameList; - pipeline?: string; - index?: string; -} - -export interface DatabaseMGetParams extends DatabaseGenericParams { - storedFields?: DatabaseNameList; - preference?: string; - realtime?: boolean; - refresh?: boolean; - _source?: DatabaseNameList; - _sourceExclude?: DatabaseNameList; - _source_includes?: DatabaseNameList; - index: string; -} - -export interface DatabaseMGetResponse { - docs?: Array>; -} - -export interface DatabasePutTemplateParams extends DatabaseGenericParams { - name: string; - body: any; -} - -export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { - waitForActiveShards?: string; - parent?: string; - refresh?: DatabaseRefresh; - routing?: string; - timeout?: string; - ifSeqNo?: number; - ifPrimaryTerm?: number; - index: string; - id: string; -} - -export interface DatabaseIndexDocumentResponse { - found: boolean; - _index: string; - _id: string; - _seq_no: number; - _primary_term: number; - result: string; -} - -export interface DatabaseUpdateDocumentResponse { - found: boolean; - _index: string; - _id: string; - _seq_no: number; - _primary_term: number; - result: string; -} - -export interface DatabaseDeleteDocumentResponse { - found: boolean; - _index: string; - _id: string; - _seq_no: number; - _primary_term: number; - result: string; -} - -export interface DatabaseIndexDocumentParams extends DatabaseGenericParams { - waitForActiveShards?: string; - opType?: 'index' | 'create'; - parent?: string; - refresh?: string; - routing?: string; - timeout?: string; - timestamp?: Date | number; - ttl?: string; - ifSeqNo?: number; - ifPrimaryTerm?: number; - pipeline?: string; - id?: string; - index: string; - body: T; -} - -export interface DatabaseGetResponse { - found: boolean; - _source: T; -} -export interface DatabaseCreateDocumentParams extends DatabaseGenericParams { - waitForActiveShards?: string; - parent?: string; - refresh?: DatabaseRefresh; - routing?: string; - timeout?: string; - timestamp?: Date | number; - ttl?: string; - ifSeqNo?: number; - ifPrimaryTerm?: number; - pipeline?: string; - id?: string; - index: string; -} - -export interface DatabaseCreateDocumentResponse { - created: boolean; - result: string; -} - -export interface DatabaseDeleteDocumentParams extends DatabaseGenericParams { - waitForActiveShards?: string; - parent?: string; - refresh?: DatabaseRefresh; - routing?: string; - timeout?: string; - ifSeqNo?: number; - ifPrimaryTerm?: number; - index: string; - id: string; -} - -export interface DatabaseGetParams extends DatabaseGenericParams { - storedFields?: DatabaseNameList; - parent?: string; - preference?: string; - realtime?: boolean; - refresh?: boolean; - routing?: string; - _source?: DatabaseNameList; - _sourceExclude?: DatabaseNameList; - _source_includes?: DatabaseNameList; - ifSeqNo?: number; - ifPrimaryTerm?: number; - id: string; - index: string; -} - -export type DatabaseNameList = string | string[] | boolean; -export type DatabaseRefresh = boolean | 'true' | 'false' | 'wait_for' | ''; -export type ExpandWildcards = 'open' | 'closed' | 'none' | 'all'; -export type DefaultOperator = 'AND' | 'OR'; -export type DatabaseConflicts = 'abort' | 'proceed'; - -export interface DatabaseGenericParams { - requestTimeout?: number; - maxRetries?: number; - method?: string; - body?: any; - ignore?: number | number[]; - filterPath?: string | string[]; -} - -export interface DatabaseDeleteDocumentResponse { - found: boolean; - _index: string; - _type: string; - _id: string; - _seq_no: number; - _primary_term: number; - result: string; -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts deleted file mode 100644 index 741668429a3c7f..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/database/kibana_database_adapter.ts +++ /dev/null @@ -1,118 +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 { ElasticsearchServiceStart, ILegacyClusterClient } from 'src/core/server'; -import { FrameworkUser } from '../framework/adapter_types'; -import { internalAuthData } from './../framework/adapter_types'; -import { - DatabaseAdapter, - DatabaseBulkIndexDocumentsParams, - DatabaseCreateDocumentParams, - DatabaseCreateDocumentResponse, - DatabaseDeleteDocumentParams, - DatabaseDeleteDocumentResponse, - DatabaseGetDocumentResponse, - DatabaseGetParams, - DatabaseIndexDocumentParams, - DatabaseMGetParams, - DatabaseMGetResponse, - DatabaseSearchParams, - DatabaseSearchResponse, -} from './adapter_types'; - -export class KibanaDatabaseAdapter implements DatabaseAdapter { - private es: ILegacyClusterClient; - - constructor(elasticsearch: ElasticsearchServiceStart) { - this.es = elasticsearch.legacy.client; - } - - public async get( - user: FrameworkUser, - params: DatabaseGetParams - ): Promise> { - return await this.callWithUser(user, 'get', params); - } - - public async mget( - user: FrameworkUser, - params: DatabaseMGetParams - ): Promise> { - return await this.callWithUser(user, 'mget', params); - } - - public async bulk(user: FrameworkUser, params: DatabaseBulkIndexDocumentsParams): Promise { - return await this.callWithUser(user, 'bulk', params); - } - - public async create( - user: FrameworkUser, - params: DatabaseCreateDocumentParams - ): Promise { - return await this.callWithUser(user, 'create', params); - } - - public async index(user: FrameworkUser, params: DatabaseIndexDocumentParams): Promise { - return await this.callWithUser(user, 'index', params); - } - - public async delete( - user: FrameworkUser, - params: DatabaseDeleteDocumentParams - ): Promise { - return await this.callWithUser(user, 'delete', params); - } - - public async deleteByQuery( - user: FrameworkUser, - params: DatabaseSearchParams - ): Promise { - return await this.callWithUser(user, 'deleteByQuery', params); - } - - public async search( - user: FrameworkUser, - params: DatabaseSearchParams - ): Promise> { - return await this.callWithUser(user, 'search', params); - } - - public async searchAll( - user: FrameworkUser, - params: DatabaseSearchParams - ): Promise> { - return await this.callWithUser(user, 'search', { - scroll: '1m', - ...params, - body: { - size: 1000, - ...params.body, - }, - }); - } - - public async putTemplate(name: string, template: any): Promise { - return await this.callWithUser({ kind: 'internal' }, 'indices.putTemplate', { - name, - body: template, - }); - } - - private callWithUser(user: FrameworkUser, esMethod: string, options: any = {}): any { - if (user.kind === 'authenticated') { - return this.es - .asScoped({ - headers: user[internalAuthData], - }) - .callAsCurrentUser(esMethod, options); - } else if (user.kind === 'internal') { - return this.es.callAsInternalUser(esMethod, options); - } else { - throw new Error('Invalid user type'); - } - } -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts deleted file mode 100644 index f4102b767c0446..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/adapter_types.ts +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import * as t from 'io-ts'; -import { Headers, KibanaRequest } from 'src/core/server'; - -export const internalAuthData = Symbol('internalAuthData'); -export const internalUser: FrameworkInternalUser = { - kind: 'internal', -}; - -export interface BackendFrameworkAdapter { - getUser(request: KibanaRequest): FrameworkUser; - internalUser: FrameworkInternalUser; - info: null | FrameworkInfo; - log(text: string): void; -} - -export const RuntimeFrameworkInfo = t.interface( - { - kibana: t.type({ - version: t.string, - }), - license: t.type({ - type: t.keyof({ - oss: null, - trial: null, - standard: null, - basic: null, - gold: null, - platinum: null, - enterprise: null, - }), - expired: t.boolean, - expiry_date_in_millis: t.number, - }), - security: t.type({ - enabled: t.boolean, - available: t.boolean, - }), - watcher: t.type({ - enabled: t.boolean, - available: t.boolean, - }), - }, - 'FrameworkInfo' -); -export interface FrameworkInfo extends t.TypeOf {} - -export const RuntimeKibanaServerRequest = t.interface( - { - params: t.object, - payload: t.object, - query: t.object, - headers: t.type({ - authorization: t.union([t.string, t.null]), - }), - info: t.type({ - remoteAddress: t.string, - }), - }, - 'KibanaServerRequest' -); -export interface KibanaServerRequest extends t.TypeOf {} - -export const RuntimeKibanaUser = t.interface( - { - username: t.string, - roles: t.readonlyArray(t.string), - full_name: t.union([t.null, t.string]), - email: t.union([t.null, t.string]), - enabled: t.boolean, - }, - 'KibanaUser' -); -export interface KibanaUser extends t.TypeOf {} - -export interface FrameworkAuthenticatedUser { - kind: 'authenticated'; - [internalAuthData]: AuthDataType; - username: string; - roles: readonly string[]; - full_name: string | null; - email: string | null; - enabled: boolean; -} - -export interface FrameworkUnAuthenticatedUser { - kind: 'unauthenticated'; -} - -export interface FrameworkInternalUser { - kind: 'internal'; -} - -export type FrameworkUser = - | FrameworkAuthenticatedUser - | FrameworkUnAuthenticatedUser - | FrameworkInternalUser; -export interface FrameworkRequest< - KibanaServerRequestGenaric extends Partial = any -> { - user: FrameworkUser; - headers: KibanaServerRequestGenaric['headers']; - info: KibanaServerRequest['info']; - payload: KibanaServerRequestGenaric['payload']; - params: KibanaServerRequestGenaric['params']; - query: KibanaServerRequestGenaric['query']; -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts deleted file mode 100644 index dcacde81ce8b39..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/framework/kibana_framework_adapter.ts +++ /dev/null @@ -1,116 +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 { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { KibanaRequest, Headers, Logger } from 'src/core/server'; -import { - BackendFrameworkAdapter, - FrameworkInfo, - FrameworkUser, - internalAuthData, - internalUser, - RuntimeFrameworkInfo, - RuntimeKibanaUser, -} from './adapter_types'; -import { BeatsManagementConfigType } from '../../../../common'; -import { ILicense, LicensingPluginStart } from '../../../../../licensing/server'; -import { SecurityPluginSetup } from '../../../../../security/server'; - -export class KibanaBackendFrameworkAdapter implements BackendFrameworkAdapter { - public readonly internalUser = internalUser; - public info: null | FrameworkInfo = null; - - constructor( - private readonly PLUGIN_ID: string, - private readonly kibanaVersion: string, - private readonly config: BeatsManagementConfigType, - private readonly logger: Logger, - private readonly licensing: LicensingPluginStart, - private readonly security?: SecurityPluginSetup - ) { - this.licensing.license$.subscribe((license) => this.licenseUpdateHandler(license)); - } - - public log(text: string) { - this.logger.info(text); - } - - getUser(request: KibanaRequest): FrameworkUser { - const user = this.security?.authc.getCurrentUser(request); - if (!user) { - return { - kind: 'unauthenticated', - }; - } - const assertKibanaUser = RuntimeKibanaUser.decode(user); - if (isLeft(assertKibanaUser)) { - throw new Error( - `Error parsing user info in ${this.PLUGIN_ID}, ${ - PathReporter.report(assertKibanaUser)[0] - }` - ); - } - - return { - kind: 'authenticated', - [internalAuthData]: request.headers, - ...user, - }; - } - - private licenseUpdateHandler = (license: ILicense) => { - let xpackInfoUnpacked: FrameworkInfo; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!license.isAvailable) { - this.info = null; - return; - } - - const securityFeature = license.getFeature('security'); - const watcherFeature = license.getFeature('watcher'); - - try { - xpackInfoUnpacked = { - kibana: { - version: this.kibanaVersion, - }, - license: { - type: license.type!, - expired: !license.isActive, - expiry_date_in_millis: license.expiryDateInMillis ?? -1, - }, - security: { - enabled: securityFeature.isEnabled, - available: securityFeature.isAvailable, - }, - watcher: { - enabled: watcherFeature.isEnabled, - available: watcherFeature.isAvailable, - }, - }; - } catch (e) { - this.logger.error(`Error accessing required xPackInfo in ${this.PLUGIN_ID} Kibana adapter`); - throw e; - } - - const assertData = RuntimeFrameworkInfo.decode(xpackInfoUnpacked); - if (isLeft(assertData)) { - throw new Error( - `Error parsing xpack info in ${this.PLUGIN_ID}, ${PathReporter.report(assertData)[0]}` - ); - } - this.info = xpackInfoUnpacked; - - return { - security: xpackInfoUnpacked.security, - settings: { ...this.config }, - }; - }; -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts deleted file mode 100644 index 6e1ad59ce0c717..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/adapter_types.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { BeatTag } from '../../../../common/domain_types'; -import { FrameworkUser } from '../framework/adapter_types'; - -export interface CMTagsAdapter { - getAll(user: FrameworkUser, ESQuery?: any): Promise; - delete(user: FrameworkUser, tagIds: string[]): Promise; - getTagsWithIds(user: FrameworkUser, tagIds: string[]): Promise; - upsertTag(user: FrameworkUser, tag: BeatTag): Promise; - getWithoutConfigTypes(user: FrameworkUser, blockTypes: string[]): Promise; -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts deleted file mode 100644 index bfc6993fcb025a..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/tags/elasticsearch_tags_adapter.ts +++ /dev/null @@ -1,180 +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 { flatten, get } from 'lodash'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { BeatTag } from '../../../../common/domain_types'; -import { DatabaseAdapter } from '../database/adapter_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { CMTagsAdapter } from './adapter_types'; - -export class ElasticsearchTagsAdapter implements CMTagsAdapter { - private database: DatabaseAdapter; - - constructor(database: DatabaseAdapter) { - this.database = database; - } - - public async getAll(user: FrameworkUser, ESQuery?: any): Promise { - const params = { - ignore: [404], - _source: true, - size: 10000, - index: INDEX_NAMES.BEATS, - body: { - query: { - bool: { - must: { - term: { - type: 'tag', - }, - }, - }, - }, - }, - }; - if (ESQuery) { - params.body.query = { - ...params.body.query, - ...ESQuery, - }; - } - const response = await this.database.search(user, params); - const tags = get(response, 'hits.hits', []) as any; - - return tags.map((tag: any) => ({ hasConfigurationBlocksTypes: [], ...tag._source.tag })); - } - - public async delete(user: FrameworkUser, tagIds: string[]): Promise { - const ids = tagIds.map((tag) => tag); - - const params = { - ignore: [404], - index: INDEX_NAMES.BEATS, - body: { - query: { - terms: { 'beat.tags': tagIds }, - }, - }, - }; - - const beatsResponse = await this.database.search(user, params); - - const beats = (get(beatsResponse, 'hits.hits', []) as BeatTag[]).map( - (beat: any) => beat._source.beat - ); - - const inactiveBeats = beats.filter((beat) => beat.active === false); - const activeBeats = beats.filter((beat) => beat.active === true); - if (activeBeats.length !== 0) { - return false; - } - const beatIds = inactiveBeats.map((beat: BeatTag) => beat.id); - - // While we block tag deletion when on an active beat, we should remove from inactive - const bulkInactiveBeatsUpdates = flatten( - beatIds.map((beatId) => { - const script = ` - def beat = ctx._source.beat; - if (beat.tags != null) { - beat.tags.removeAll([params.tag]); - }`; - - return flatten( - ids.map((tagId) => [ - { update: { _id: `beat:${beatId}` } }, - { script: { source: script.replace(' ', ''), params: { tagId } } }, - ]) - ); - }) - ); - - const bulkTagsDelete = ids.map((tagId) => ({ delete: { _id: `tag:${tagId}` } })); - - await this.database.bulk(user, { - body: flatten([...bulkInactiveBeatsUpdates, ...bulkTagsDelete]), - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }); - - return true; - } - - public async getTagsWithIds(user: FrameworkUser, tagIds: string[]): Promise { - if (tagIds.length === 0) { - return []; - } - const ids = tagIds.map((tag) => `tag:${tag}`); - - const params = { - ignore: [404], - _source: true, - body: { - ids, - }, - index: INDEX_NAMES.BEATS, - }; - const response = await this.database.mget(user, params); - - return get(response, 'docs', []) - .filter((b: any) => b.found) - .map((b: any) => ({ - hasConfigurationBlocksTypes: [], - ...b._source.tag, - id: b._id.replace('tag:', ''), - })); - } - - public async upsertTag(user: FrameworkUser, tag: BeatTag): Promise { - const body = { - tag, - type: 'tag', - }; - - const params = { - body, - id: `tag:${tag.id}`, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }; - const response = await this.database.index(user, params); - - return get(response, 'result') as string; - } - - public async getWithoutConfigTypes( - user: FrameworkUser, - blockTypes: string[] - ): Promise { - const body = { - query: { - bool: { - filter: { - match: { - type: 'tag', - }, - }, - must_not: { - terms: { 'tag.hasConfigurationBlocksTypes': blockTypes }, - }, - }, - }, - }; - - const params = { - body, - index: INDEX_NAMES.BEATS, - ignore: [404], - _source: true, - size: 10000, - }; - const response = await this.database.search(user, params); - const tags = get(response, 'hits.hits', []) as any; - - return tags.map((tag: any) => ({ hasConfigurationBlocksTypes: [], ...tag._source.tag })); - } -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts deleted file mode 100644 index a7d45490ada387..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/tokens/adapter_types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FrameworkUser } from '../framework/adapter_types'; - -export interface TokenEnrollmentData { - token: string | null; - expires_on: string; -} - -export interface CMTokensAdapter { - deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise; - getEnrollmentToken(user: FrameworkUser, enrollmentToken: string): Promise; - insertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]): Promise; -} diff --git a/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts b/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts deleted file mode 100644 index 849602564347e4..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/adapters/tokens/elasticsearch_tokens_adapter.ts +++ /dev/null @@ -1,76 +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 { flatten, get } from 'lodash'; -import { INDEX_NAMES } from '../../../../common/constants'; -import { DatabaseAdapter } from '../database/adapter_types'; -import { FrameworkUser } from '../framework/adapter_types'; -import { CMTokensAdapter, TokenEnrollmentData } from './adapter_types'; - -export class ElasticsearchTokensAdapter implements CMTokensAdapter { - constructor(private readonly database: DatabaseAdapter) {} - - public async deleteEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { - const params = { - id: `enrollment_token:${enrollmentToken}`, - index: INDEX_NAMES.BEATS, - }; - - await this.database.delete(user, params); - } - - public async getEnrollmentToken( - user: FrameworkUser, - tokenString: string - ): Promise { - const params = { - id: `enrollment_token:${tokenString}`, - ignore: [404], - index: INDEX_NAMES.BEATS, - }; - - const response = await this.database.get(user, params); - - const tokenDetails = get(response, '_source.enrollment_token', { - expires_on: '0', - token: null, - }) as TokenEnrollmentData; - - // Elasticsearch might return fast if the token is not found. OR it might return fast - // if the token *is* found. Either way, an attacker could using a timing attack to figure - // out whether a token is valid or not. So we introduce a random delay in returning from - // this function to obscure the actual time it took for Elasticsearch to find the token. - const randomDelayInMs = 25 + Math.round(Math.random() * 200); // between 25 and 225 ms - return new Promise((resolve) => - setTimeout(() => resolve(tokenDetails), randomDelayInMs) - ); - } - - public async insertTokens(user: FrameworkUser, tokens: TokenEnrollmentData[]) { - const body = flatten( - tokens.map((token) => [ - { index: { _id: `enrollment_token:${token.token}` } }, - { - enrollment_token: token, - type: 'enrollment_token', - }, - ]) - ); - - const result = await this.database.bulk(user, { - body, - index: INDEX_NAMES.BEATS, - refresh: 'wait_for', - }); - - if (result.errors) { - throw new Error(result.items[0].result); - } - - return tokens; - } -} diff --git a/x-pack/plugins/beats_management/server/lib/beat_events.ts b/x-pack/plugins/beats_management/server/lib/beat_events.ts deleted file mode 100644 index ffe77f8d99fffa..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/beat_events.ts +++ /dev/null @@ -1,59 +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 { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { BeatEvent, RuntimeBeatEvent } from '../../common/domain_types'; -import { FrameworkUser } from './adapters/framework/adapter_types'; -import { CMBeatsDomain } from './beats'; - -export class BeatEventsLib { - constructor(private readonly beats: CMBeatsDomain) {} - - public log = async ( - user: FrameworkUser, - beatId: string, - events: BeatEvent[] - ): Promise> => { - return events.map((event, i) => { - const assertData = RuntimeBeatEvent.decode(event); - if (isLeft(assertData)) { - if (events.length - 1 === i) { - this.beats - .update(user, beatId, { - status: { - ...events[events.length - 2], - timestamp: new Date(events[events.length - 2].timestamp), - }, - }) - .catch((e) => { - // eslint-disable-next-line - console.error('Error inserting event into beats log.', e); - }); - } - return { - success: false, - error: `Error parsing event ${i}, ${PathReporter.report(assertData)[0]}`, - }; - } - if (events.length - 1 === i) { - this.beats - .update(user, beatId, { - status: { - ...events[events.length - 1], - timestamp: new Date(events[events.length - 1].timestamp), - }, - }) - .catch((e) => { - // eslint-disable-next-line - console.error('Error inserting event into beats log.', e); - }); - } - return { success: true }; - }); - }; -} diff --git a/x-pack/plugins/beats_management/server/lib/beats.ts b/x-pack/plugins/beats_management/server/lib/beats.ts deleted file mode 100644 index de3fce4011a3d1..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/beats.ts +++ /dev/null @@ -1,260 +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 { uniq } from 'lodash'; -import moment from 'moment'; -import { CMBeat } from '../../common/domain_types'; -import { - BeatsRemovalReturn, - BeatsTagAssignment, - CMAssignmentReturn, - CMBeatsAdapter, -} from './adapters/beats/adapter_types'; -import { FrameworkUser } from './adapters/framework/adapter_types'; -import { BeatEnrollmentStatus, CMServerLibs, UserOrToken } from './types'; - -export class CMBeatsDomain { - private tags: CMServerLibs['tags']; - private tokens: CMServerLibs['tokens']; - private framework: CMServerLibs['framework']; - - constructor( - private readonly adapter: CMBeatsAdapter, - libs: { - tags: CMServerLibs['tags']; - tokens: CMServerLibs['tokens']; - framework: CMServerLibs['framework']; - } - ) { - this.adapter = adapter; - this.tags = libs.tags; - this.tokens = libs.tokens; - this.framework = libs.framework; - } - - public async getById(user: FrameworkUser, beatId: string): Promise { - const beat = await this.adapter.get(user, beatId); - return beat && beat.active ? beat : null; - } - - public async getByIds(user: FrameworkUser, beatIds: string[]): Promise { - const beats = await this.adapter.getWithIds(user, beatIds); - return beats.filter((beat) => beat.active); - } - - public async getAll(user: FrameworkUser, ESQuery?: any) { - return (await this.adapter.getAll(user, ESQuery)).filter( - (beat: CMBeat) => beat.active === true - ); - } - - public async getAllWithTag(user: FrameworkUser, tagId: string) { - return (await this.adapter.getAllWithTags(user, [tagId])).filter( - (beat: CMBeat) => beat.active === true - ); - } - - public async getByEnrollmentToken(user: FrameworkUser, enrollmentToken: string) { - const beat = await this.adapter.getBeatWithToken(user, enrollmentToken); - return beat && beat.active ? beat : null; - } - - public async update(userOrToken: UserOrToken, beatId: string, beatData: Partial) { - const beat = await this.adapter.get(this.framework.internalUser, beatId); - - // FIXME make return type enum - if (beat === null) { - return 'beat-not-found'; - } - - if (typeof userOrToken === 'string') { - const { verified: isAccessTokenValid } = this.tokens.verifyToken( - beat?.access_token ?? '', - userOrToken - ); - if (!isAccessTokenValid) { - return 'invalid-access-token'; - } - } - - const user = typeof userOrToken === 'string' ? this.framework.internalUser : userOrToken; - await this.adapter.update(user, { - ...beat, - ...beatData, - }); - } - - public async enrollBeat( - enrollmentToken: string, - beatId: string, - remoteAddress: string, - beat: Partial - ): Promise<{ status: string; accessToken?: string }> { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { token, expires_on } = await this.tokens.getEnrollmentToken(enrollmentToken); - if (expires_on && moment(expires_on).isBefore(moment())) { - return { status: BeatEnrollmentStatus.ExpiredEnrollmentToken }; - } - if (!token) { - return { status: BeatEnrollmentStatus.InvalidEnrollmentToken }; - } - - const existingBeat = await this.getById(this.framework.internalUser, beatId); - if (existingBeat) { - return { status: BeatEnrollmentStatus.Success }; - } - - const accessToken = this.tokens.generateAccessToken(); - const verifiedOn = moment().toJSON(); - - await this.adapter.insert(this.framework.internalUser, { - tags: [], - ...beat, - active: true, - enrollment_token: enrollmentToken, - verified_on: verifiedOn, - access_token: accessToken, - host_ip: remoteAddress, - id: beatId, - } as CMBeat); - - await this.tokens.deleteEnrollmentToken(enrollmentToken); - - return { status: BeatEnrollmentStatus.Success, accessToken }; - } - - public async removeTagsFromBeats( - user: FrameworkUser, - removals: BeatsTagAssignment[] - ): Promise { - const beatIds = uniq(removals.map((removal) => removal.beatId)); - const tagIds = uniq(removals.map((removal) => removal.tag)); - - const response = { - removals: removals.map(() => ({ status: null })), - }; - - const beats = await this.adapter.getWithIds(user, beatIds); - const tags = await this.tags.getWithIds(user, tagIds); - - // Handle assignments containing non-existing beat IDs or tags - const nonExistentBeatIds = findNonExistentItems(beats, beatIds); - const nonExistentTags = await findNonExistentItems(tags, tagIds); - - addNonExistentItemToResponse( - response, - removals, - nonExistentBeatIds, - nonExistentTags, - 'removals' - ); - - // FIXME abstract this - const validRemovals = removals - .map((removal, idxInRequest) => ({ - beatId: removal.beatId, - idxInRequest, // so we can add the result of this removal to the correct place in the response - tag: removal.tag, - })) - .filter((removal, idx) => response.removals[idx].status === null); - - if (validRemovals.length > 0) { - const removalResults = await this.adapter.removeTagsFromBeats(user, validRemovals); - return addToResultsToResponse('removals', response, removalResults); - } - return response; - } - - public async assignTagsToBeats( - user: FrameworkUser, - assignments: BeatsTagAssignment[] - ): Promise { - const beatIds = uniq(assignments.map((assignment) => assignment.beatId)); - const tagIds = uniq(assignments.map((assignment) => assignment.tag)); - - const response = { - assignments: assignments.map(() => ({ status: null })), - }; - const beats = await this.adapter.getWithIds(user, beatIds); - const tags = await this.tags.getWithIds(user, tagIds); - // Handle assignments containing non-existing beat IDs or tags - const nonExistentBeatIds = findNonExistentItems(beats, beatIds); - const nonExistentTags = findNonExistentItems(tags, tagIds); - - // FIXME break out back into route / function response - // FIXME causes function to error if a beat or tag does not exist - addNonExistentItemToResponse( - response, - assignments, - nonExistentBeatIds, - nonExistentTags, - 'assignments' - ); - - // FIXME abstract this - const validAssignments = assignments - .map((assignment, idxInRequest) => ({ - beatId: assignment.beatId, - idxInRequest, // so we can add the result of this assignment to the correct place in the response - tag: assignment.tag, - })) - .filter((assignment, idx) => response.assignments[idx].status === null); - - if (validAssignments.length > 0) { - const assignmentResults = await this.adapter.assignTagsToBeats(user, validAssignments); - - // TODO This should prob not mutate - return addToResultsToResponse('assignments', response, assignmentResults); - } - return response; - } -} - -// FIXME abstract to the route, also the key arg is a temp fix -function addNonExistentItemToResponse( - response: any, - assignments: any, - nonExistentBeatIds: any, - nonExistentTags: any, - key: string -) { - assignments.forEach(({ beatId, tag }: BeatsTagAssignment, idx: any) => { - const isBeatNonExistent = nonExistentBeatIds.includes(beatId); - - const isTagNonExistent = nonExistentTags.includes(tag); - - if (isBeatNonExistent && isTagNonExistent) { - response[key][idx].status = 404; - response[key][idx].result = `Beat ${beatId} and tag ${tag} not found`; - } else if (isBeatNonExistent) { - response[key][idx].status = 404; - response[key][idx].result = `Beat ${beatId} not found`; - } else if (isTagNonExistent) { - response[key][idx].status = 404; - response[key][idx].result = `Tag ${tag} not found`; - } - }); -} - -// TODO dont mutate response -function addToResultsToResponse(key: string, response: any, assignmentResults: any) { - assignmentResults.forEach((assignmentResult: any) => { - const { idxInRequest, status, result } = assignmentResult; - response[key][idxInRequest].status = status; - response[key][idxInRequest].result = result; - }); - return response; -} - -export function findNonExistentItems(items: Array<{ id: string }>, requestedItems: string[]) { - return requestedItems.reduce((nonExistentItems: string[], requestedItem: string, idx: number) => { - if (items.findIndex((item) => item && item.id === requestedItem) === -1) { - nonExistentItems.push(requestedItems[idx]); - } - return nonExistentItems; - }, []); -} diff --git a/x-pack/plugins/beats_management/server/lib/compose/kibana.ts b/x-pack/plugins/beats_management/server/lib/compose/kibana.ts deleted file mode 100644 index d3606bf24071df..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/compose/kibana.ts +++ /dev/null @@ -1,85 +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 { camelCase } from 'lodash'; -import type { ElasticsearchServiceStart, Logger } from 'src/core/server'; -import { SecurityPluginSetup } from '../../../../security/server'; -import { LicensingPluginStart } from '../../../../licensing/server'; -import { PLUGIN } from '../../../common/constants'; -import { BeatsManagementConfigType } from '../../../common'; -import { ElasticsearchBeatsAdapter } from '../adapters/beats/elasticsearch_beats_adapter'; -import { ElasticsearchConfigurationBlockAdapter } from '../adapters/configuration_blocks/elasticsearch_configuration_block_adapter'; -import { KibanaDatabaseAdapter } from '../adapters/database/kibana_database_adapter'; -import { KibanaBackendFrameworkAdapter } from '../adapters/framework/kibana_framework_adapter'; -import { ElasticsearchTagsAdapter } from '../adapters/tags/elasticsearch_tags_adapter'; -import { ElasticsearchTokensAdapter } from '../adapters/tokens/elasticsearch_tokens_adapter'; -import { BeatEventsLib } from '../beat_events'; -import { CMBeatsDomain } from '../beats'; -import { ConfigurationBlocksLib } from '../configuration_blocks'; -import { CMTagsDomain } from '../tags'; -import { CMTokensDomain } from '../tokens'; -import { CMServerLibs } from '../types'; -import { BackendFrameworkLib } from './../framework'; - -interface ComposeOptions { - elasticsearch: ElasticsearchServiceStart; - licensing: LicensingPluginStart; - security?: SecurityPluginSetup; - config: BeatsManagementConfigType; - logger: Logger; - kibanaVersion: string; -} - -export function compose({ - elasticsearch, - config, - kibanaVersion, - logger, - licensing, - security, -}: ComposeOptions): CMServerLibs { - const backendAdapter = new KibanaBackendFrameworkAdapter( - camelCase(PLUGIN.ID), - kibanaVersion, - config, - logger, - licensing, - security - ); - const framework = new BackendFrameworkLib(backendAdapter, config); - const database = new KibanaDatabaseAdapter(elasticsearch); - const beatsAdapter = new ElasticsearchBeatsAdapter(database); - const configAdapter = new ElasticsearchConfigurationBlockAdapter(database); - - const tags = new CMTagsDomain( - new ElasticsearchTagsAdapter(database), - configAdapter, - beatsAdapter - ); - const configurationBlocks = new ConfigurationBlocksLib(configAdapter, tags); - const tokens = new CMTokensDomain(new ElasticsearchTokensAdapter(database), { - framework, - }); - const beats = new CMBeatsDomain(beatsAdapter, { - tags, - tokens, - framework, - }); - const beatEvents = new BeatEventsLib(beats); - - const libs: CMServerLibs = { - beatEvents, - framework, - database, - beats, - tags, - tokens, - configurationBlocks, - }; - - return libs; -} diff --git a/x-pack/plugins/beats_management/server/lib/configuration_blocks.ts b/x-pack/plugins/beats_management/server/lib/configuration_blocks.ts deleted file mode 100644 index 06f1674af93fb0..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/configuration_blocks.ts +++ /dev/null @@ -1,77 +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 { UNIQUENESS_ENFORCING_TYPES } from '../../common/constants/configuration_blocks'; -import { ConfigurationBlock } from '../../common/domain_types'; -import { ConfigurationBlockAdapter } from './adapters/configuration_blocks/adapter_types'; -import { FrameworkUser } from './adapters/framework/adapter_types'; -import { CMTagsDomain } from './tags'; - -export class ConfigurationBlocksLib { - constructor( - private readonly adapter: ConfigurationBlockAdapter, - private readonly tags: CMTagsDomain - ) {} - - public async getForTags( - user: FrameworkUser, - tagIds: string[], - page: number = 0, - size: number = 10 - ) { - if ((page + 1) * size > 10000) { - throw new Error('System error, too many results. To get all results, request page: -1'); - } - - const result = await this.adapter.getForTags(user, tagIds, page, size); - - return { ...result, error: null }; - } - - public async delete(user: FrameworkUser, ids: string[]) { - return await this.adapter.delete(user, ids); - } - - public async save(user: FrameworkUser, block: ConfigurationBlock) { - const tags = await this.tags.getWithIds(user, [block.tag]); - const tag = tags[0]; - - if (!tag) { - return { - error: 'Invalid tag, tag not found', - }; - } - - if (!tag.hasConfigurationBlocksTypes) { - tag.hasConfigurationBlocksTypes = []; - } - - if ( - !block.id && - UNIQUENESS_ENFORCING_TYPES.includes(block.type) && - tag.hasConfigurationBlocksTypes.some((type: string) => - UNIQUENESS_ENFORCING_TYPES.includes(type) - ) - ) { - return { - error: - 'Block is of type that already exists on this tag, and only one config of this type can exist at a time on a beat. Config not saved', - }; - } - - if (UNIQUENESS_ENFORCING_TYPES.includes(block.type)) { - tag.hasConfigurationBlocksTypes.push(block.type); - await this.tags.upsertTag(user, tag); - } - - const ids = await this.adapter.create(user, [block]); - return { - success: true, - blockID: ids[0], - }; - } -} diff --git a/x-pack/plugins/beats_management/server/lib/framework.ts b/x-pack/plugins/beats_management/server/lib/framework.ts deleted file mode 100644 index 072bbae6d46c0a..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/framework.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Headers, KibanaRequest } from 'kibana/server'; -import { BackendFrameworkAdapter, FrameworkUser } from './adapters/framework/adapter_types'; -import { BeatsManagementConfigType } from '../../common'; - -export class BackendFrameworkLib { - public log = this.adapter.log; - public internalUser = this.adapter.internalUser; - - constructor( - private readonly adapter: BackendFrameworkAdapter, - private readonly config: BeatsManagementConfigType - ) { - this.validateConfig(); - } - - public getConfig(): BeatsManagementConfigType { - return this.config; - } - - public getUser(request: KibanaRequest): FrameworkUser { - return this.adapter.getUser(request); - } - - /** - * Expired `null` happens when we have no xpack info - */ - public get license() { - return { - type: this.adapter.info ? this.adapter.info.license.type : 'unknown', - expired: this.adapter.info ? this.adapter.info.license.expired : null, - }; - } - - public get securityIsEnabled() { - return this.adapter.info ? this.adapter.info.security.enabled : false; - } - - private validateConfig() { - const encryptionKey = this.config.encryptionKey; - - if (!encryptionKey) { - this.adapter.log( - 'Using a default encryption key for xpack.beats.encryptionKey. It is recommended that you set xpack.beats.encryptionKey in kibana.yml with a unique token' - ); - } - } -} diff --git a/x-pack/plugins/beats_management/server/lib/tags.ts b/x-pack/plugins/beats_management/server/lib/tags.ts deleted file mode 100644 index 2750e73b898b06..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/tags.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 { uniq } from 'lodash'; -import { UNIQUENESS_ENFORCING_TYPES } from '../../common/constants/configuration_blocks'; -import { BeatTag } from '../../common/domain_types'; -import { CMBeatsAdapter } from './adapters/beats/adapter_types'; -import { ConfigurationBlockAdapter } from './adapters/configuration_blocks/adapter_types'; -import { FrameworkUser } from './adapters/framework/adapter_types'; -import { CMTagsAdapter } from './adapters/tags/adapter_types'; - -export class CMTagsDomain { - constructor( - private readonly adapter: CMTagsAdapter, - private readonly configurationBlocksAdapter: ConfigurationBlockAdapter, - private readonly beatsAdabter: CMBeatsAdapter - ) {} - - public async getAll(user: FrameworkUser, ESQuery?: any): Promise { - const tags = await this.adapter.getAll(user, ESQuery); - return tags; - } - - public async getWithIds(user: FrameworkUser, tagIds: string[]): Promise { - const tags = await this.adapter.getTagsWithIds(user, tagIds); - return tags; - } - - public async delete(user: FrameworkUser, tagIds: string[]) { - const beats = await this.beatsAdabter.getAllWithTags(user, tagIds); - if (beats.filter((b) => b.active).length > 0) { - return false; - } - await this.configurationBlocksAdapter.deleteForTags(user, tagIds); - return await this.adapter.delete(user, tagIds); - } - - public async getNonConflictingTags(user: FrameworkUser, existingTagIds: string[]) { - const tags = await this.adapter.getTagsWithIds(user, existingTagIds); - const existingUniqueBlockTypes = uniq( - tags.reduce((existingUniqueTypes, tag) => { - if (tag.hasConfigurationBlocksTypes) { - existingUniqueTypes = existingUniqueTypes.concat(tag.hasConfigurationBlocksTypes); - } - return existingUniqueTypes; - }, [] as string[]) - ).filter((type) => UNIQUENESS_ENFORCING_TYPES.includes(type)); - - const safeTags = await this.adapter.getWithoutConfigTypes(user, existingUniqueBlockTypes); - return safeTags; - } - - public async upsertTag(user: FrameworkUser, tag: BeatTag): Promise { - const tagId = await this.adapter.upsertTag(user, tag); - - return tagId; - } -} diff --git a/x-pack/plugins/beats_management/server/lib/tokens.ts b/x-pack/plugins/beats_management/server/lib/tokens.ts deleted file mode 100644 index 8ff130190f661a..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/tokens.ts +++ /dev/null @@ -1,144 +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 { randomBytes, timingSafeEqual } from 'crypto'; -import { sign as signToken, verify as verifyToken } from 'jsonwebtoken'; -import { chunk } from 'lodash'; -import moment from 'moment'; -import { FrameworkUser } from './adapters/framework/adapter_types'; -import { CMTokensAdapter } from './adapters/tokens/adapter_types'; -import { CMServerLibs } from './types'; - -const RANDOM_TOKEN_1 = 'b48c4bda384a40cb91c6eb9b8849e77f'; -const RANDOM_TOKEN_2 = '80a3819e3cd64f4399f1d4886be7a08b'; - -export class CMTokensDomain { - private adapter: CMTokensAdapter; - private framework: CMServerLibs['framework']; - - constructor(adapter: CMTokensAdapter, libs: { framework: CMServerLibs['framework'] }) { - this.adapter = adapter; - this.framework = libs.framework; - } - - public async getEnrollmentToken(enrollmentToken: string) { - const fullToken = await this.adapter.getEnrollmentToken( - this.framework.internalUser, - enrollmentToken - ); - if (!fullToken) { - return { - token: null, - expired: true, - expires_on: null, - }; - } - - const { verified, expired } = this.verifyToken(enrollmentToken, fullToken.token || '', false); - - if (!verified) { - return { - expired, - token: null, - expires_on: null, - }; - } - - return { ...fullToken, expired }; - } - - public async deleteEnrollmentToken(enrollmentToken: string) { - return await this.adapter.deleteEnrollmentToken(this.framework.internalUser, enrollmentToken); - } - - public verifyToken(recivedToken: string, token2: string, decode = true) { - let tokenDecoded = true; - let expired = false; - - if (decode) { - const enrollmentTokenSecret = this.framework.getConfig().encryptionKey; - - try { - verifyToken(recivedToken, enrollmentTokenSecret); - tokenDecoded = true; - } catch (err) { - if (err.name === 'TokenExpiredError') { - expired = true; - } - tokenDecoded = false; - } - } - - if ( - typeof recivedToken !== 'string' || - typeof token2 !== 'string' || - recivedToken.length !== token2.length - ) { - // This prevents a more subtle timing attack where we know already the tokens aren't going to - // match but still we don't return fast. Instead we compare two pre-generated random tokens using - // the same comparison algorithm that we would use to compare two equal-length tokens. - return { - expired, - verified: - timingSafeEqual( - Buffer.from(RANDOM_TOKEN_1, 'utf8'), - Buffer.from(RANDOM_TOKEN_2, 'utf8') - ) && tokenDecoded, - }; - } - - return { - expired, - verified: - timingSafeEqual(Buffer.from(recivedToken, 'utf8'), Buffer.from(token2, 'utf8')) && - tokenDecoded, - }; - } - - public generateAccessToken() { - const enrollmentTokenSecret = this.framework.getConfig().encryptionKey; - - const tokenData = { - created: moment().toJSON(), - randomHash: randomBytes(26).toString(), - }; - - return signToken(tokenData, enrollmentTokenSecret); - } - - public async createEnrollmentTokens( - user: FrameworkUser, - numTokens: number = 1 - ): Promise { - const tokens = []; - const enrollmentTokensTtlInSeconds = this.framework.getConfig().enrollmentTokensTtlInSeconds; - - const enrollmentTokenExpiration = moment() - .add(enrollmentTokensTtlInSeconds, 'seconds') - .toJSON(); - const enrollmentTokenSecret = this.framework.getConfig().encryptionKey; - - while (tokens.length < numTokens) { - const tokenData = { - created: moment().toJSON(), - expires: enrollmentTokenExpiration, - randomHash: randomBytes(26).toString(), - }; - - tokens.push({ - expires_on: enrollmentTokenExpiration, - token: signToken(tokenData, enrollmentTokenSecret), - }); - } - - await Promise.all( - chunk(tokens, 100).map((tokenChunk) => this.adapter.insertTokens(user, tokenChunk)) - ); - - return tokens.map((token) => token.token); - } -} diff --git a/x-pack/plugins/beats_management/server/lib/types.ts b/x-pack/plugins/beats_management/server/lib/types.ts deleted file mode 100644 index 8342018469450b..00000000000000 --- a/x-pack/plugins/beats_management/server/lib/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { IRouter, RequestHandlerContext } from 'src/core/server'; -import { DatabaseAdapter } from './adapters/database/adapter_types'; -import { FrameworkUser } from './adapters/framework/adapter_types'; -import { BeatEventsLib } from './beat_events'; -import { CMBeatsDomain } from './beats'; -import { ConfigurationBlocksLib } from './configuration_blocks'; -import { BackendFrameworkLib } from './framework'; -import { CMTagsDomain } from './tags'; -import { CMTokensDomain } from './tokens'; - -export type UserOrToken = FrameworkUser | string; - -export interface CMServerLibs { - framework: BackendFrameworkLib; - database: DatabaseAdapter; - beats: CMBeatsDomain; - tags: CMTagsDomain; - beatEvents: BeatEventsLib; - tokens: CMTokensDomain; - configurationBlocks: ConfigurationBlocksLib; -} - -export enum BeatEnrollmentStatus { - Success = 'Success', - ExpiredEnrollmentToken = 'Expired enrollment token', - InvalidEnrollmentToken = 'Invalid enrollment token', -} - -export interface AsyncResponse { - error: { - code: number | string; - message: string; - }; -} -export interface AsyncResponse { - data: DataType; -} - -/** - * @internal - */ -export type BeatsManagementApiRequestHandlerContext = CMServerLibs; - -/** - * @internal - */ -export interface BeatsManagementRequestHandlerContext extends RequestHandlerContext { - beatsManagement: CMServerLibs; -} - -/** - * @internal - */ -export type BeatsManagementRouter = IRouter; diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts deleted file mode 100644 index 3093d5d9b8d299..00000000000000 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ /dev/null @@ -1,89 +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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext, Logger } from 'src/core/server'; -import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; -import { SecurityPluginSetup } from '../../security/server'; -import { LicensingPluginStart } from '../../licensing/server'; -import { BeatsManagementConfigType } from '../common'; -import type { BeatsManagementRequestHandlerContext, CMServerLibs } from './lib/types'; -import { registerRoutes } from './routes'; -import { compose } from './lib/compose/kibana'; -import { INDEX_NAMES } from '../common/constants'; -import { beatsIndexTemplate } from './index_templates'; - -interface SetupDeps { - security?: SecurityPluginSetup; - features: FeaturesPluginSetup; -} - -interface StartDeps { - licensing: LicensingPluginStart; -} - -export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDeps> { - private readonly logger: Logger; - private securitySetup?: SecurityPluginSetup; - private beatsLibs?: CMServerLibs; - - constructor( - private readonly initializerContext: PluginInitializerContext - ) { - this.logger = initializerContext.logger.get(); - } - - public setup(core: CoreSetup, { features, security }: SetupDeps) { - this.securitySetup = security; - - const router = core.http.createRouter(); - registerRoutes(router); - - core.http.registerRouteHandlerContext( - 'beatsManagement', - (_, req) => { - return this.beatsLibs!; - } - ); - - features.registerElasticsearchFeature({ - id: 'beats_management', - management: { - ingest: ['beats_management'], - }, - privileges: [ - { - ui: [], - requiredClusterPrivileges: [], - requiredRoles: ['beats_admin'], - }, - ], - }); - - return {}; - } - - public start({ elasticsearch }: CoreStart, { licensing }: StartDeps) { - const config = this.initializerContext.config.get(); - const logger = this.initializerContext.logger.get(); - const kibanaVersion = this.initializerContext.env.packageInfo.version; - - this.beatsLibs = compose({ - elasticsearch, - licensing, - security: this.securitySetup, - config, - logger, - kibanaVersion, - }); - - this.beatsLibs.database.putTemplate(INDEX_NAMES.BEATS, beatsIndexTemplate).catch((e) => { - this.logger.error(`Error create beats template: ${e.message}`); - }); - - return {}; - } -} diff --git a/x-pack/plugins/beats_management/server/routes/beats/configuration.ts b/x-pack/plugins/beats_management/server/routes/beats/configuration.ts deleted file mode 100644 index 2adbe46524c6a8..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/configuration.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { ConfigurationBlock } from '../../../common/domain_types'; -import { ReturnTypeList } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerGetBeatConfigurationRoute = (router: BeatsManagementRouter) => { - router.get( - { - path: '/api/beats/agent/{beatId}/configuration', - validate: { - params: schema.object({ - beatId: schema.string(), - }), - }, - options: { - authRequired: false, - }, - }, - wrapRouteWithSecurity({}, async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const accessToken = request.headers['kbn-beats-access-token']; - if (!accessToken) { - return response.badRequest({ - body: 'beats access token required', - }); - } - const beatId = request.params.beatId; - - let configurationBlocks: ConfigurationBlock[]; - const beat = await beatsManagement.beats.getById( - beatsManagement.framework.internalUser, - beatId - ); - if (beat === null) { - return response.notFound({ - body: { - message: `Beat "${beatId}" not found`, - }, - }); - } - - const isAccessTokenValid = beat.access_token === accessToken; - if (!isAccessTokenValid) { - return response.unauthorized({ - body: { - message: 'Invalid access token', - }, - }); - } - - await beatsManagement.beats.update(beatsManagement.framework.internalUser, beat.id, { - last_checkin: new Date(), - }); - - if (beat.tags) { - const result = await beatsManagement.configurationBlocks.getForTags( - beatsManagement.framework.internalUser, - beat.tags, - -1 - ); - - configurationBlocks = result.blocks; - } else { - configurationBlocks = []; - } - - return response.ok({ - body: { - list: configurationBlocks, - success: true, - } as ReturnTypeList, - }); - }) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/enroll.ts b/x-pack/plugins/beats_management/server/routes/beats/enroll.ts deleted file mode 100644 index 8af93a30b61b57..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/enroll.ts +++ /dev/null @@ -1,90 +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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ensureRawRequest } from '../../../../../../src/core/server/http/router'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BeatEnrollmentStatus } from '../../lib/types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerBeatEnrollmentRoute = (router: BeatsManagementRouter) => { - // TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 - router.post( - { - path: '/api/beats/agent/{beatId}', - validate: { - params: schema.object({ - beatId: schema.string(), - }), - body: schema.object( - { - host_name: schema.string(), - name: schema.string(), - type: schema.string(), - version: schema.string(), - }, - { unknowns: 'ignore' } - ), - }, - options: { - authRequired: false, - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - - const { beatId } = request.params; - const enrollmentToken = request.headers['kbn-beats-enrollment-token'] as string; - if (!enrollmentToken) { - return response.badRequest({ - body: 'beats enrollment token required', - }); - } - - // TODO: fixme eventually, need to access `info.remoteAddress` from KibanaRequest. - const legacyRequest = ensureRawRequest(request); - - const { status, accessToken } = await beatsManagement.beats.enrollBeat( - enrollmentToken, - beatId, - legacyRequest.info.remoteAddress, - request.body - ); - - switch (status) { - case BeatEnrollmentStatus.ExpiredEnrollmentToken: - return response.badRequest({ - body: { - message: BeatEnrollmentStatus.ExpiredEnrollmentToken, - }, - }); - case BeatEnrollmentStatus.InvalidEnrollmentToken: - return response.badRequest({ - body: { - message: BeatEnrollmentStatus.InvalidEnrollmentToken, - }, - }); - case BeatEnrollmentStatus.Success: - default: - return response.ok({ - body: { - item: accessToken, - action: 'created', - success: true, - }, - }); - } - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/events.ts b/x-pack/plugins/beats_management/server/routes/beats/events.ts deleted file mode 100644 index 8df18224845ea4..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/events.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { ReturnTypeBulkAction } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerBeatEventsRoute = (router: BeatsManagementRouter) => { - router.post( - { - path: '/api/beats/{beatId}/events', - validate: { - params: schema.object({ - beatId: schema.string(), - }), - body: schema.arrayOf(schema.any(), { defaultValue: [] }), - }, - options: { - authRequired: false, - }, - }, - wrapRouteWithSecurity({}, async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const accessToken = request.headers['kbn-beats-access-token']; - if (!accessToken) { - return response.badRequest({ - body: 'beats access token required', - }); - } - const beatId = request.params.beatId; - const events = request.body; - const internalUser = beatsManagement.framework.internalUser; - - const beat = await beatsManagement.beats.getById(internalUser, beatId); - if (beat === null) { - return response.badRequest({ - body: { - message: `Beat "${beatId}" not found`, - }, - }); - } - - const isAccessTokenValid = beat.access_token === accessToken; - if (!isAccessTokenValid) { - return response.unauthorized({ - body: { - message: `Invalid access token`, - }, - }); - } - - const results = await beatsManagement.beatEvents.log(internalUser, beat.id, events); - - return response.ok({ - body: { - results, - success: true, - } as ReturnTypeBulkAction, - }); - }) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/get.ts b/x-pack/plugins/beats_management/server/routes/beats/get.ts deleted file mode 100644 index e9a31030831434..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/get.ts +++ /dev/null @@ -1,60 +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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { CMBeat } from '../../../common/domain_types'; -import { ReturnTypeGet } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerGetBeatRoute = (router: BeatsManagementRouter) => { - router.get( - { - path: '/api/beats/agent/{beatId}/{token?}', - validate: { - params: schema.object({ - beatId: schema.string(), - token: schema.string({ defaultValue: '' }), - }), - }, - }, - wrapRouteWithSecurity( - { requiredRoles: ['beats_admin'] }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - const beatId = request.params.beatId; - - let beat: CMBeat | null; - if (beatId === 'unknown') { - beat = await beatsManagement.beats.getByEnrollmentToken(user, request.params.token); - if (beat === null) { - return response.ok({ body: { success: false } }); - } - } else { - beat = await beatsManagement.beats.getById(user, beatId); - if (beat === null) { - return response.notFound({ - body: { - message: 'Beat not found', - }, - }); - } - } - - delete beat.access_token; - - return response.ok({ - body: { - item: beat, - success: true, - } as ReturnTypeGet, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/index.ts b/x-pack/plugins/beats_management/server/routes/beats/index.ts deleted file mode 100644 index c39248b6766ae4..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { registerGetBeatConfigurationRoute } from './configuration'; -export { registerBeatEnrollmentRoute } from './enroll'; -export { registerBeatEventsRoute } from './events'; -export { registerGetBeatRoute } from './get'; -export { registerListAgentsRoute } from './list'; -export { registerTagAssignmentsRoute } from './tag_assignment'; -export { registerTagRemovalsRoute } from './tag_removal'; -export { registerBeatUpdateRoute } from './update'; diff --git a/x-pack/plugins/beats_management/server/routes/beats/list.ts b/x-pack/plugins/beats_management/server/routes/beats/list.ts deleted file mode 100644 index 549c274bfc1ada..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/list.ts +++ /dev/null @@ -1,70 +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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { CMBeat } from '../../../common/domain_types'; -import { ReturnTypeList } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerListAgentsRoute = (router: BeatsManagementRouter) => { - router.get( - { - path: '/api/beats/agents/{listByAndValue*}', - validate: { - params: schema.object({ - listByAndValue: schema.maybe(schema.string()), - }), - query: schema.object( - { - ESQuery: schema.maybe(schema.string()), - }, - { defaultValue: {} } - ), - }, - }, - wrapRouteWithSecurity( - { - requiredRoles: ['beats_admin'], - requiredLicense: REQUIRED_LICENSES, - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement; - const user = beatsManagement.framework.getUser(request); - - const listByAndValueParts = request.params.listByAndValue?.split('/') ?? []; - let listBy: string | null = null; - let listByValue: string | null = null; - if (listByAndValueParts.length === 2) { - listBy = listByAndValueParts[0]; - listByValue = listByAndValueParts[1]; - } - - let beats: CMBeat[]; - - switch (listBy) { - case 'tag': - beats = await beatsManagement.beats.getAllWithTag(user, listByValue || ''); - break; - - default: - beats = await beatsManagement.beats.getAll( - user, - request.query.ESQuery ? JSON.parse(request.query.ESQuery) : undefined - ); - - break; - } - - return response.ok({ - body: { list: beats, success: true, page: -1, total: -1 } as ReturnTypeList, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/tag_assignment.ts b/x-pack/plugins/beats_management/server/routes/beats/tag_assignment.ts deleted file mode 100644 index 44274ac8af4011..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/tag_assignment.ts +++ /dev/null @@ -1,69 +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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkAction } from '../../../common/return_types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import type { BeatsTagAssignment } from '../../../public/lib/adapters/beats/adapter_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerTagAssignmentsRoute = (router: BeatsManagementRouter) => { - // TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 - router.post( - { - path: '/api/beats/agents_tags/assignments', - validate: { - body: schema.object({ - assignments: schema.arrayOf( - schema.object({ - beatId: schema.string(), - tag: schema.string(), - }), - { defaultValue: [] } - ), - }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - const { assignments }: { assignments: BeatsTagAssignment[] } = request.body; - - const result = await beatsManagement.beats.assignTagsToBeats(user, assignments); - - return response.ok({ - body: { - success: true, - results: result.assignments.map((assignment) => ({ - success: assignment.status && assignment.status >= 200 && assignment.status < 300, - error: - !assignment.status || assignment.status >= 300 - ? { - code: assignment.status || 400, - message: assignment.result, - } - : undefined, - result: - assignment.status && assignment.status >= 200 && assignment.status < 300 - ? { - message: assignment.result, - } - : undefined, - })), - } as ReturnTypeBulkAction, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/tag_removal.ts b/x-pack/plugins/beats_management/server/routes/beats/tag_removal.ts deleted file mode 100644 index d00b16c4ae176f..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/tag_removal.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkAction } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerTagRemovalsRoute = (router: BeatsManagementRouter) => { - // TODO: write to Kibana audit log file https://github.com/elastic/kibana/issues/26024 - router.post( - { - path: '/api/beats/agents_tags/removals', - validate: { - body: schema.object({ - removals: schema.arrayOf( - schema.object({ - beatId: schema.string(), - tag: schema.string(), - }), - { defaultValue: [] } - ), - }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement; - const user = beatsManagement.framework.getUser(request); - const { removals } = request.body; - - const result = await beatsManagement.beats.removeTagsFromBeats(user, removals); - - return response.ok({ - body: { - success: true, - results: result.removals.map((removal) => ({ - success: removal.status && removal.status >= 200 && removal.status < 300, - error: - !removal.status || removal.status >= 300 - ? { - code: removal.status || 400, - message: removal.result, - } - : undefined, - result: - removal.status && removal.status >= 200 && removal.status < 300 - ? { - message: removal.result, - } - : undefined, - })), - } as ReturnTypeBulkAction, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/beats/update.ts b/x-pack/plugins/beats_management/server/routes/beats/update.ts deleted file mode 100644 index 982005bcf84e93..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/beats/update.ts +++ /dev/null @@ -1,105 +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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ensureRawRequest } from '../../../../../../src/core/server/http/router'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { CMBeat } from '../../../common/domain_types'; -import { ReturnTypeUpdate } from '../../../common/return_types'; -import { internalUser } from '../../lib/adapters/framework/adapter_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerBeatUpdateRoute = (router: BeatsManagementRouter) => { - // TODO: write to Kibana audit log file (include who did the verification as well) https://github.com/elastic/kibana/issues/26024 - router.put( - { - path: '/api/beats/agent/{beatId}', - validate: { - params: schema.object({ - beatId: schema.string(), - }), - body: schema.object( - { - active: schema.maybe(schema.boolean()), - ephemeral_id: schema.maybe(schema.string()), - host_name: schema.maybe(schema.string()), - local_configuration_yml: schema.maybe(schema.string()), - metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())), - name: schema.maybe(schema.string()), - type: schema.maybe(schema.string()), - version: schema.maybe(schema.string()), - }, - { defaultValue: {} } - ), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement; - const accessToken = request.headers['kbn-beats-access-token'] as string; - const { beatId } = request.params; - const user = beatsManagement.framework.getUser(request); - const userOrToken = accessToken || user; - - // TODO: fixme eventually, need to access `info.remoteAddress` from KibanaRequest. - const legacyRequest = ensureRawRequest(request); - const remoteAddress = legacyRequest.info.remoteAddress; - - if (user.kind === 'unauthenticated' && request.body.active !== undefined) { - return response.unauthorized({ - body: { - message: 'access-token is not a valid auth type to change beat status', - }, - }); - } - - const status = await beatsManagement.beats.update(userOrToken, beatId, { - ...request.body, - host_ip: remoteAddress, - }); - - switch (status) { - case 'beat-not-found': - return response.notFound({ - body: { - message: 'Beat not found', - }, - }); - case 'invalid-access-token': - return response.unauthorized({ - body: { - message: 'Invalid access token', - }, - }); - } - - const beat = await beatsManagement.beats.getById(internalUser, beatId); - if (!beat) { - return response.notFound({ - body: { - message: 'Beat not found', - }, - }); - } - - return response.ok({ - body: { - item: beat, - action: 'updated', - success: true, - } as ReturnTypeUpdate, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/configurations/delete.ts b/x-pack/plugins/beats_management/server/routes/configurations/delete.ts deleted file mode 100644 index 2596204017c114..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/configurations/delete.ts +++ /dev/null @@ -1,48 +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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkDelete } from '../../../common/return_types'; - -export const registerDeleteConfigurationBlocksRoute = (router: BeatsManagementRouter) => { - router.delete( - { - path: '/api/beats/configurations/{ids}', - validate: { - params: schema.object({ - ids: schema.string(), - }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement; - const ids = request.params.ids.split(',').filter((id) => id.length > 0); - const user = beatsManagement.framework.getUser(request); - - const results = await beatsManagement.configurationBlocks.delete(user, ids); - return response.ok({ - body: { - success: true, - results: results.map((result) => ({ - success: result.success, - action: 'deleted', - error: result.success ? undefined : { message: result.reason }, - })), - } as ReturnTypeBulkDelete, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/configurations/get.ts b/x-pack/plugins/beats_management/server/routes/configurations/get.ts deleted file mode 100644 index 8018420bda7733..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/configurations/get.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ConfigurationBlock } from '../../../common/domain_types'; -import { ReturnTypeList } from '../../../common/return_types'; - -export const registerGetConfigurationBlocksRoute = (router: BeatsManagementRouter) => { - router.get( - { - path: '/api/beats/configurations/{tagIds}/{page?}', - validate: { - params: schema.object({ - tagIds: schema.string(), - page: schema.maybe(schema.number()), - }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement; - const tagIds = request.params.tagIds.split(',').filter((id) => id.length > 0); - const user = beatsManagement.framework.getUser(request); - const result = await beatsManagement.configurationBlocks.getForTags( - user, - tagIds, - request.params.page, - 5 - ); - - return response.ok({ - body: { - page: result.page, - total: result.total, - list: result.blocks, - success: true, - } as ReturnTypeList, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/configurations/index.ts b/x-pack/plugins/beats_management/server/routes/configurations/index.ts deleted file mode 100644 index f1e0875c8919de..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/configurations/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { registerGetConfigurationBlocksRoute } from './get'; -export { registerDeleteConfigurationBlocksRoute } from './delete'; -export { registerUpsertConfigurationBlocksRoute } from './upsert'; diff --git a/x-pack/plugins/beats_management/server/routes/configurations/upsert.ts b/x-pack/plugins/beats_management/server/routes/configurations/upsert.ts deleted file mode 100644 index d490cadabf236c..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/configurations/upsert.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { isLeft } from 'fp-ts/lib/Either'; -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants'; -import { - ConfigurationBlock, - createConfigurationBlockInterface, -} from '../../../common/domain_types'; -import { ReturnTypeBulkUpsert } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerUpsertConfigurationBlocksRoute = (router: BeatsManagementRouter) => { - // TODO: write to Kibana audit log file - router.put( - { - path: '/api/beats/configurations', - validate: { - body: schema.arrayOf(schema.recordOf(schema.string(), schema.any()), { defaultValue: [] }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement; - const user = beatsManagement.framework.getUser(request); - const input = request.body as ConfigurationBlock[]; - - const result = await Promise.all( - input.map(async (block: ConfigurationBlock) => { - const assertData = createConfigurationBlockInterface().decode(block); - if (isLeft(assertData)) { - return { - error: `Error parsing block info, ${PathReporter.report(assertData)[0]}`, - }; - } - - const { blockID, success, error } = await beatsManagement.configurationBlocks.save( - user, - block - ); - if (error) { - return { success, error }; - } - - return { success, blockID }; - }) - ); - - return response.ok({ - body: { - results: result.map((r) => ({ - success: r.success as boolean, - // TODO: we need to surface this data, not hard coded - action: 'created' as 'created' | 'updated', - })), - success: true, - } as ReturnTypeBulkUpsert, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/index.ts b/x-pack/plugins/beats_management/server/routes/index.ts deleted file mode 100644 index 8ea1ca9502959f..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/index.ts +++ /dev/null @@ -1,55 +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 { BeatsManagementRouter } from '../lib/types'; -import { - registerDeleteConfigurationBlocksRoute, - registerGetConfigurationBlocksRoute, - registerUpsertConfigurationBlocksRoute, -} from './configurations'; -import { registerCreateTokenRoute } from './tokens'; -import { - registerSetTagRoute, - registerListTagsRoute, - registerGetTagsWithIdsRoute, - registerDeleteTagsWithIdsRoute, - registerAssignableTagsRoute, -} from './tags'; -import { - registerBeatUpdateRoute, - registerTagRemovalsRoute, - registerTagAssignmentsRoute, - registerListAgentsRoute, - registerGetBeatRoute, - registerBeatEventsRoute, - registerBeatEnrollmentRoute, - registerGetBeatConfigurationRoute, -} from './beats'; - -export const registerRoutes = (router: BeatsManagementRouter) => { - // configurations - registerGetConfigurationBlocksRoute(router); - registerDeleteConfigurationBlocksRoute(router); - registerUpsertConfigurationBlocksRoute(router); - // beats - registerBeatUpdateRoute(router); - registerTagRemovalsRoute(router); - registerTagAssignmentsRoute(router); - registerListAgentsRoute(router); - registerGetBeatRoute(router); - registerBeatEventsRoute(router); - registerBeatEnrollmentRoute(router); - registerGetBeatConfigurationRoute(router); - // tags - registerSetTagRoute(router); - registerListTagsRoute(router); - registerGetTagsWithIdsRoute(router); - registerDeleteTagsWithIdsRoute(router); - registerAssignableTagsRoute(router); - // tokens - registerCreateTokenRoute(router); -}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/assignable.ts b/x-pack/plugins/beats_management/server/routes/tags/assignable.ts deleted file mode 100644 index 9ab89dbcfbcd4e..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/tags/assignable.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import { flatten } from 'lodash'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeBulkGet } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerAssignableTagsRoute = (router: BeatsManagementRouter) => { - router.get( - { - path: '/api/beats/tags/assignable/{beatIds}', - validate: { - params: schema.object({ - beatIds: schema.string(), - }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - const beatIds = request.params.beatIds.split(',').filter((id) => id.length > 0); - - const beats = await beatsManagement.beats.getByIds(user, beatIds); - const tags = await beatsManagement.tags.getNonConflictingTags( - user, - flatten(beats.map((beat) => beat.tags)) - ); - - return response.ok({ - body: { - items: tags, - success: true, - } as ReturnTypeBulkGet, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/delete.ts b/x-pack/plugins/beats_management/server/routes/tags/delete.ts deleted file mode 100644 index 54b3cf03a08bb3..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/tags/delete.ts +++ /dev/null @@ -1,48 +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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkDelete } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerDeleteTagsWithIdsRoute = (router: BeatsManagementRouter) => { - router.delete( - { - path: '/api/beats/tags/{tagIds}', - validate: { - params: schema.object({ - tagIds: schema.string(), - }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - const tagIds = request.params.tagIds.split(',').filter((id) => id.length > 0); - - const success = await beatsManagement.tags.delete(user, tagIds); - - return response.ok({ - body: { - results: tagIds.map(() => ({ - success, - action: 'deleted', - })), - success, - } as ReturnTypeBulkDelete, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/get.ts b/x-pack/plugins/beats_management/server/routes/tags/get.ts deleted file mode 100644 index 2fe1eb8ab5515e..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/tags/get.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 { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeBulkGet } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerGetTagsWithIdsRoute = (router: BeatsManagementRouter) => { - router.get( - { - path: '/api/beats/tags/{tagIds}', - validate: { - params: schema.object({ - tagIds: schema.string(), - }), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - const tagIds = request.params.tagIds.split(',').filter((id) => id.length > 0); - - const tags = await beatsManagement.tags.getWithIds(user, tagIds); - - return response.ok({ - body: { - items: tags, - success: true, - } as ReturnTypeBulkGet, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/index.ts b/x-pack/plugins/beats_management/server/routes/tags/index.ts deleted file mode 100644 index a4208a56793599..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/tags/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { registerAssignableTagsRoute } from './assignable'; -export { registerDeleteTagsWithIdsRoute } from './delete'; -export { registerGetTagsWithIdsRoute } from './get'; -export { registerListTagsRoute } from './list'; -export { registerSetTagRoute } from './set'; diff --git a/x-pack/plugins/beats_management/server/routes/tags/list.ts b/x-pack/plugins/beats_management/server/routes/tags/list.ts deleted file mode 100644 index cee2231808aa63..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/tags/list.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeList } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerListTagsRoute = (router: BeatsManagementRouter) => { - router.get( - { - path: '/api/beats/tags', - validate: { - query: schema.object( - { - ESQuery: schema.maybe(schema.string()), - }, - { defaultValue: {} } - ), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - - const tags = await beatsManagement.tags.getAll( - user, - request.query && request.query.ESQuery ? JSON.parse(request.query.ESQuery) : undefined - ); - - return response.ok({ - body: { - list: tags, - success: true, - page: -1, - total: -1, - } as ReturnTypeList, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/tags/set.ts b/x-pack/plugins/beats_management/server/routes/tags/set.ts deleted file mode 100644 index b556531e8c113a..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/tags/set.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants'; -import { BeatTag } from '../../../common/domain_types'; -import { ReturnTypeUpsert } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -export const registerSetTagRoute = (router: BeatsManagementRouter) => { - // TODO: write to Kibana audit log file - router.put( - { - path: '/api/beats/tag/{tagId}', - validate: { - params: schema.object({ - tagId: schema.string(), - }), - body: schema.object( - { - color: schema.maybe(schema.string()), - name: schema.maybe(schema.string()), - }, - { defaultValue: {} } - ), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - - const config = { - id: request.params.tagId, - name: request.params.tagId, - color: '#DD0A73', - hasConfigurationBlocksTypes: [], - ...request.body, - }; - const id = await beatsManagement.tags.upsertTag(user, config); - const tag = await beatsManagement.tags.getWithIds(user, [id]); - - // TODO the action needs to be surfaced - return response.ok({ - body: { - success: true, - item: tag[0], - action: 'created', - } as ReturnTypeUpsert, - }); - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/tokens/create.ts b/x-pack/plugins/beats_management/server/routes/tokens/create.ts deleted file mode 100644 index c44f9c2dd4e7dc..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/tokens/create.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { schema } from '@kbn/config-schema'; -import type { BeatsManagementRouter } from '../../lib/types'; -import { REQUIRED_LICENSES } from '../../../common/constants/security'; -import { ReturnTypeBulkCreate } from '../../../common/return_types'; -import { wrapRouteWithSecurity } from '../wrap_route_with_security'; - -const DEFAULT_NUM_TOKENS = 1; - -export const registerCreateTokenRoute = (router: BeatsManagementRouter) => { - // TODO: write to Kibana audit log file - router.post( - { - path: '/api/beats/enrollment_tokens', - validate: { - body: schema.nullable( - schema.object({ - num_tokens: schema.number({ defaultValue: DEFAULT_NUM_TOKENS, min: 1 }), - }) - ), - }, - }, - wrapRouteWithSecurity( - { - requiredLicense: REQUIRED_LICENSES, - requiredRoles: ['beats_admin'], - }, - async (context, request, response) => { - const beatsManagement = context.beatsManagement!; - const user = beatsManagement.framework.getUser(request); - - const numTokens = request.body?.num_tokens ?? DEFAULT_NUM_TOKENS; - try { - const tokens = await beatsManagement.tokens.createEnrollmentTokens(user, numTokens); - return response.ok({ - body: { - results: tokens.map((token) => ({ - item: token, - success: true, - action: 'created', - })), - success: true, - } as ReturnTypeBulkCreate, - }); - } catch (err) { - beatsManagement.framework.log(err.message); - throw new Error('An error occurred, please check your Kibana logs'); - } - } - ) - ); -}; diff --git a/x-pack/plugins/beats_management/server/routes/wrap_route_with_security.ts b/x-pack/plugins/beats_management/server/routes/wrap_route_with_security.ts deleted file mode 100644 index 327d599ec27036..00000000000000 --- a/x-pack/plugins/beats_management/server/routes/wrap_route_with_security.ts +++ /dev/null @@ -1,70 +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 { KibanaRequest, KibanaResponseFactory, RequestHandler, RouteMethod } from 'src/core/server'; -import { difference } from 'lodash'; -import type { BeatsManagementRequestHandlerContext } from '../lib/types'; - -export function wrapRouteWithSecurity< - P, - Q, - B, - Context extends BeatsManagementRequestHandlerContext ->( - { - requiredLicense = [], - requiredRoles = [], - }: { requiredLicense?: string[]; requiredRoles?: string[] }, - handler: RequestHandler -): RequestHandler { - return async ( - context: Context, - request: KibanaRequest, - response: KibanaResponseFactory - ) => { - const beatsManagement = context.beatsManagement!; - const license = beatsManagement.framework.license; - const user = beatsManagement.framework.getUser(request); - - if ( - requiredLicense.length > 0 && - (license.expired || !requiredLicense.includes(license.type)) - ) { - return response.forbidden({ - body: { - message: `Your ${license.type} license does not support this API or is expired. Please upgrade your license.`, - }, - }); - } - - if (requiredRoles.length > 0) { - if (user.kind !== 'authenticated') { - return response.forbidden({ - body: { - message: `Request must be authenticated`, - }, - }); - } - - if ( - user.kind === 'authenticated' && - !user.roles.includes('superuser') && - difference(requiredRoles, user.roles).length !== 0 - ) { - return response.forbidden({ - body: { - message: `Request must be authenticated by a user with one of the following user roles: ${requiredRoles.join( - ',' - )}`, - }, - }); - } - } - - return handler(context, request, response); - }; -} diff --git a/x-pack/plugins/beats_management/tsconfig.json b/x-pack/plugins/beats_management/tsconfig.json deleted file mode 100644 index ad68cc900e6382..00000000000000 --- a/x-pack/plugins/beats_management/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "composite": true, - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - "types/**/*", - "scripts/**/*", - "server/index_templates/*.json", - ], - "references": [ - { "path": "../../../src/core/tsconfig.json" }, - { "path": "../../../src/plugins/data/tsconfig.json" }, - { "path": "../../../src/plugins/management/tsconfig.json" }, - { "path": "../licensing/tsconfig.json" }, - { "path": "../features/tsconfig.json" }, - { "path": "../security/tsconfig.json" } - ] -} diff --git a/x-pack/plugins/beats_management/types/formsy.d.ts b/x-pack/plugins/beats_management/types/formsy.d.ts deleted file mode 100644 index 46231b2385991c..00000000000000 --- a/x-pack/plugins/beats_management/types/formsy.d.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -declare module 'formsy-react' { - import React, { FC } from 'react'; - let Formsy: FC; - export interface FormsyInputProps { - getErrorMessage(): any; - getValue(): any; - hasValue(): boolean; - isFormDisabled(): boolean; - isFormSubmitted(): boolean; - isPristine(): boolean; - isRequired(): boolean; - isValid(): boolean; - isValidValue(): boolean; - resetValue(): void; - setValidations(validations: any, required: boolean): void; - setValue(value: any): void; - showError(): boolean; - showRequired(): boolean; - } - - // eslint-disable-next-line import/no-default-export - export default Formsy; - export type FormData = { [fieldName in keyof FormShape]: string }; - export type FieldValue = string | null | undefined; - - type ValidationMethod = ( - values: FormData, - value: string | null | undefined - ) => void; - - export function addValidationRule( - validationName: string, - validationMethod: ValidationMethod - ): void; - export function withFormsy(component: any): any; - - // function withFormsy( - // component: - // | React.Component - // | FC - // ): React.Component; -} diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 34fade58ffc8dd..e85840e8734308 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -33,7 +33,6 @@ module.exports = { // Include a require alias for legacy UI code and styles resolve: { alias: { - ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), 'data/interpreter': path.resolve( KIBANA_ROOT, 'src/plugins/data/public/expressions/interpreter' diff --git a/x-pack/plugins/canvas/storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js index 3051bbebdaf0c8..c13fabe9989219 100644 --- a/x-pack/plugins/canvas/storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -77,13 +77,6 @@ module.exports = { filename: 'dll.js', library: DLL_NAME, }, - // Include a require alias for legacy UI code and styles - resolve: { - alias: { - ui: path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'), - }, - symlinks: false, - }, module: { rules: [ { diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx index 0070bc18dfe121..86f80f772944aa 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.test.tsx @@ -73,7 +73,14 @@ describe('ConnectorsDropdown', () => { > @@ -96,7 +103,14 @@ describe('ConnectorsDropdown', () => { > @@ -119,7 +133,14 @@ describe('ConnectorsDropdown', () => { > @@ -142,7 +163,14 @@ describe('ConnectorsDropdown', () => { > @@ -182,7 +210,9 @@ describe('ConnectorsDropdown', () => { wrappingComponent: TestProviders, }); - expect(newWrapper.find('button span:not([data-euiicon-type])').text()).toEqual('My Connector'); + expect(newWrapper.find('button span:not([data-euiicon-type])').at(1).text()).toBe( + 'My Connector' + ); }); test('if the props hideConnectorServiceNowSir is true, the connector should not be part of the list of options ', () => { diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts index fc2f66d331700c..1657153ab645bd 100644 --- a/x-pack/plugins/cases/public/components/connectors/types.ts +++ b/x-pack/plugins/cases/public/components/connectors/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { IconType } from '@elastic/eui/src/components/icon/icon'; import React from 'react'; import { @@ -26,7 +27,7 @@ export interface ThirdPartyField { export interface ConnectorConfiguration { name: string; - logo: string; + logo: IconType; } export interface CaseConnector { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/kibana_header_actions.test.tsx new file mode 100644 index 00000000000000..21fc2b235d83cf --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/kibana_header_actions.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 { setMockValues } from '../../../__mocks__'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButtonEmpty } from '@elastic/eui'; + +import { KibanaHeaderActions } from './kibana_header_actions'; + +describe('KibanaHeaderActions', () => { + const values = { + engineName: 'foo', + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + }); + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonEmpty).exists()).toBe(true); + }); + + it('does not render a "Query Tester" button if there is no engine available', () => { + setMockValues({ + engineName: '', + }); + const wrapper = shallow(); + expect(wrapper.find(EuiButtonEmpty).exists()).toBe(false); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/kibana_header_actions.tsx new file mode 100644 index 00000000000000..b2e810962df029 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/layout/kibana_header_actions.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues } from 'kea'; + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { EngineLogic } from '../engine'; + +export const KibanaHeaderActions: React.FC = () => { + const { engineName } = useValues(EngineLogic); + + return ( + + {engineName && ( + + + {i18n.translate('xpack.enterpriseSearch.appSearch.engine.queryTesterButtonLabel', { + defaultMessage: 'Query tester', + })} + + + )} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 2a7f256398381b..287d46c2dec75c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -7,6 +7,7 @@ import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { setMockValues, rerender } from '../__mocks__'; +import '../__mocks__/shallow_useeffect.mock'; import '../__mocks__/enterprise_search_url.mock'; import '../__mocks__/react_router_history.mock'; @@ -70,9 +71,10 @@ describe('AppSearchUnconfigured', () => { describe('AppSearchConfigured', () => { let wrapper: ShallowWrapper; + const renderHeaderActions = jest.fn(); beforeAll(() => { - setMockValues({ myRole: {} }); + setMockValues({ myRole: {}, renderHeaderActions }); wrapper = shallow(); }); @@ -83,6 +85,10 @@ describe('AppSearchConfigured', () => { expect(wrapper.find(EngineRouter)).toHaveLength(1); }); + it('renders header actions', () => { + expect(renderHeaderActions).toHaveBeenCalled(); + }); + it('mounts AppLogic with passed initial data props', () => { expect(AppLogic).toHaveBeenCalledWith(DEFAULT_INITIAL_APP_DATA); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 0b87321d875354..9b59e0e19a5da1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import { Route, Redirect, Switch, useRouteMatch } from 'react-router-dom'; import { useValues } from 'kea'; @@ -25,6 +25,7 @@ import { EngineNav, EngineRouter } from './components/engine'; import { EngineCreation } from './components/engine_creation'; import { EnginesOverview, ENGINES_TITLE } from './components/engines'; import { ErrorConnecting } from './components/error_connecting'; +import { KibanaHeaderActions } from './components/layout/kibana_header_actions'; import { Library } from './components/library'; import { MetaEngineCreation } from './components/meta_engine_creation'; import { RoleMappingsRouter } from './components/role_mappings'; @@ -77,8 +78,13 @@ export const AppSearchConfigured: React.FC> = (props) = const { myRole: { canManageEngines, canManageMetaEngines, canViewRoleMappings }, } = useValues(AppLogic(props)); + const { renderHeaderActions } = useValues(KibanaLogic); const { readOnlyMode } = useValues(HttpLogic); + useEffect(() => { + renderHeaderActions(KibanaHeaderActions); + }, []); + return ( {process.env.NODE_ENV === 'development' && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx index 0875e8cf0ec089..1dddf54faa7aff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { EuiButtonEmpty, EuiText, EuiFlexGroup, EuiFlexItem, EuiHeaderLinks } from '@elastic/eui'; +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiHeaderLinks } from '@elastic/eui'; import { externalUrl, getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { EuiButtonEmptyTo } from '../../../shared/react_router_helpers'; @@ -25,8 +25,9 @@ export const WorkplaceSearchHeaderActions: React.FC = () => { data-test-subj="PersonalDashboardButton" iconType="user" to={PERSONAL_SOURCES_PATH} + size="s" > - {NAV.PERSONAL_DASHBOARD} + {NAV.PERSONAL_DASHBOARD} @@ -35,8 +36,9 @@ export const WorkplaceSearchHeaderActions: React.FC = () => { href={getWorkplaceSearchUrl('/search')} target="_blank" iconType="search" + size="s" > - {NAV.SEARCH} + {NAV.SEARCH} diff --git a/x-pack/plugins/features/common/feature_kibana_privileges.ts b/x-pack/plugins/features/common/feature_kibana_privileges.ts index 7febba197647d0..166ce5b62a0670 100644 --- a/x-pack/plugins/features/common/feature_kibana_privileges.ts +++ b/x-pack/plugins/features/common/feature_kibana_privileges.ts @@ -82,27 +82,50 @@ export interface FeatureKibanaPrivileges { * Alert Types and Alert Types provided by other features to which you wish to grant access. */ alerting?: { - /** - * List of alert types which users should have full read/write access to when granted this privilege. - * @example - * ```ts - * { - * all: ['my-alert-type-within-my-feature'] - * } - * ``` - */ - all?: readonly string[]; - - /** - * List of alert types which users should have read-only access to when granted this privilege. - * @example - * ```ts - * { - * read: ['my-alert-type'] - * } - * ``` - */ - read?: readonly string[]; + rule?: { + /** + * List of rule types which users should have full read/write access to when granted this privilege. + * @example + * ```ts + * { + * all: ['my-alert-type-within-my-feature'] + * } + * ``` + */ + all?: readonly string[]; + /** + * List of rule types which users should have read-only access to when granted this privilege. + * @example + * ```ts + * { + * read: ['my-alert-type'] + * } + * ``` + */ + read?: readonly string[]; + }; + alert?: { + /** + * List of rule types for which users should have full read/write access their alert data to when granted this privilege. + * @example + * ```ts + * { + * all: ['my-alert-type-within-my-feature'] + * } + * ``` + */ + all?: readonly string[]; + /** + * List of rule types for which users should have read-only access to their alert data when granted this privilege. + * @example + * ```ts + * { + * read: ['my-alert-type'] + * } + * ``` + */ + read?: readonly string[]; + }; }; /** * If your feature requires access to specific saved objects, then specify your access needs here. diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index 88712f2ac14c03..64be725e02bbe5 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -512,8 +512,14 @@ Array [ Object { "privilege": Object { "alerting": Object { - "all": Array [], - "read": Array [], + "alert": Object { + "all": Array [], + "read": Array [], + }, + "rule": Object { + "all": Array [], + "read": Array [], + }, }, "api": Array [ "store_search_session", @@ -651,8 +657,14 @@ Array [ Object { "privilege": Object { "alerting": Object { - "all": Array [], - "read": Array [], + "alert": Object { + "all": Array [], + "read": Array [], + }, + "rule": Object { + "all": Array [], + "read": Array [], + }, }, "api": Array [ "store_search_session", @@ -888,8 +900,14 @@ Array [ Object { "privilege": Object { "alerting": Object { - "all": Array [], - "read": Array [], + "alert": Object { + "all": Array [], + "read": Array [], + }, + "rule": Object { + "all": Array [], + "read": Array [], + }, }, "api": Array [], "app": Array [ @@ -1010,8 +1028,14 @@ Array [ Object { "privilege": Object { "alerting": Object { - "all": Array [], - "read": Array [], + "alert": Object { + "all": Array [], + "read": Array [], + }, + "rule": Object { + "all": Array [], + "read": Array [], + }, }, "api": Array [ "store_search_session", @@ -1149,8 +1173,14 @@ Array [ Object { "privilege": Object { "alerting": Object { - "all": Array [], - "read": Array [], + "alert": Object { + "all": Array [], + "read": Array [], + }, + "rule": Object { + "all": Array [], + "read": Array [], + }, }, "api": Array [ "store_search_session", @@ -1386,8 +1416,14 @@ Array [ Object { "privilege": Object { "alerting": Object { - "all": Array [], - "read": Array [], + "alert": Object { + "all": Array [], + "read": Array [], + }, + "rule": Object { + "all": Array [], + "read": Array [], + }, }, "api": Array [], "app": Array [ diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts index 6acc29793797fe..75e6eaa4020917 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -46,8 +46,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + alert: { + all: ['alerting-all-type'], + read: [], + }, }, ui: ['ui-action'], }, @@ -63,7 +69,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -93,8 +104,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + alert: { + all: ['alerting-all-type'], + read: [], + }, }, ui: ['ui-action'], }, @@ -113,7 +130,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -139,8 +161,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + alert: { + all: ['alerting-all-type'], + read: ['alerting-read-type-alerts'], + }, }, ui: ['ui-action'], }, @@ -156,7 +184,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -187,8 +220,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + alert: { + all: ['alerting-all-type'], + read: ['alerting-read-type-alerts'], + }, }, ui: ['ui-action'], }, @@ -212,11 +251,15 @@ describe('featurePrivilegeIterator', () => { }, savedObject: { all: ['all-type'], - read: ['read-type'], + read: [], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -232,7 +275,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -259,8 +307,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + alert: { + all: ['alerting-all-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -290,11 +339,15 @@ describe('featurePrivilegeIterator', () => { }, savedObject: { all: ['all-type'], - read: ['read-type'], + read: [], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -313,7 +366,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -340,8 +398,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -357,7 +419,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -384,8 +451,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + alert: { + all: ['alerting-all-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -418,8 +486,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -438,7 +510,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -465,8 +542,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -482,7 +563,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -510,8 +596,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + alert: { + all: ['alerting-all-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -545,8 +632,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type', 'read-sub-type'], }, alerting: { - all: ['alerting-all-type', 'alerting-all-sub-type'], - read: ['alerting-read-type', 'alerting-read-sub-type'], + rule: { + all: ['alerting-all-type'], + read: [], + }, + alert: { + all: ['alerting-all-sub-type'], + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action', 'ui-sub-type'], }, @@ -566,8 +659,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type', 'read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-type', 'alerting-read-sub-type'], + rule: { + all: [], + read: ['alerting-read-type'], + }, + alert: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action', 'ui-sub-type'], }, @@ -594,8 +693,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + alert: { + all: [], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -611,7 +716,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -638,7 +748,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + alert: { + all: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -671,8 +783,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: ['alerting-read-type'], + }, + alert: { + all: ['alerting-read-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -691,8 +809,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: [], - read: ['alerting-read-type'], + rule: { + all: [], + read: ['alerting-read-type'], + }, + alert: { + all: ['alerting-read-type'], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -719,8 +843,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -736,7 +864,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -764,8 +897,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + alert: { + all: ['alerting-all-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -799,8 +933,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type', 'read-sub-type'], }, alerting: { - all: ['alerting-all-type', 'alerting-all-sub-type'], - read: ['alerting-read-type', 'alerting-read-sub-type'], + rule: { + all: ['alerting-all-type'], + read: [], + }, + alert: { + all: ['alerting-all-sub-type'], + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action', 'ui-sub-type'], }, @@ -819,7 +959,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -846,8 +991,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -863,7 +1012,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -892,8 +1046,9 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + alert: { + all: ['alerting-all-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -926,8 +1081,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -946,7 +1105,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -999,8 +1163,10 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, }, ui: ['ui-sub-type'], }, @@ -1034,8 +1200,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, + alert: { + all: [], + read: [], + }, }, ui: ['ui-sub-type'], }, @@ -1055,8 +1227,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-sub-type'], }, alerting: { - all: ['alerting-all-sub-type'], - read: ['alerting-read-sub-type'], + rule: { + all: ['alerting-all-sub-type'], + read: ['alerting-read-sub-type'], + }, + alert: { + all: [], + read: [], + }, }, ui: ['ui-sub-type'], }, @@ -1083,8 +1261,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + }, + alert: { + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -1100,7 +1282,12 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - read: ['alerting-read-type'], + rule: { + read: ['alerting-read-type'], + }, + alert: { + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, @@ -1151,8 +1338,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: ['alerting-all-type'], - read: ['alerting-read-type'], + rule: { + all: ['alerting-all-type'], + read: [], + }, + alert: { + all: [], + read: ['alerting-another-read-type'], + }, }, ui: ['ui-action'], }, @@ -1171,8 +1364,14 @@ describe('featurePrivilegeIterator', () => { read: ['read-type'], }, alerting: { - all: [], - read: ['alerting-read-type'], + rule: { + all: [], + read: ['alerting-read-type'], + }, + alert: { + all: [], + read: ['alerting-read-type'], + }, }, ui: ['ui-action'], }, diff --git a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts index e194a051c8a6e5..b58f72b0fadc06 100644 --- a/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/features/server/feature_privilege_iterator/feature_privilege_iterator.ts @@ -110,11 +110,26 @@ function mergeWithSubFeatures( ); mergedConfig.alerting = { - all: mergeArrays(mergedConfig.alerting?.all ?? [], subFeaturePrivilege.alerting?.all ?? []), - read: mergeArrays( - mergedConfig.alerting?.read ?? [], - subFeaturePrivilege.alerting?.read ?? [] - ), + rule: { + all: mergeArrays( + mergedConfig.alerting?.rule?.all ?? [], + subFeaturePrivilege.alerting?.rule?.all ?? [] + ), + read: mergeArrays( + mergedConfig.alerting?.rule?.read ?? [], + subFeaturePrivilege.alerting?.rule?.read ?? [] + ), + }, + alert: { + all: mergeArrays( + mergedConfig.alerting?.alert?.all ?? [], + subFeaturePrivilege.alerting?.alert?.all ?? [] + ), + read: mergeArrays( + mergedConfig.alerting?.alert?.read ?? [], + subFeaturePrivilege.alerting?.alert?.read ?? [] + ), + }, }; } return mergedConfig; diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index 0eb00b43d6f5d4..8e7ed45f33f50f 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -828,7 +828,7 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { + it(`prevents privileges from specifying alerting/rule entries that don't exist at the root level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -838,8 +838,57 @@ describe('FeatureRegistry', () => { privileges: { all: { alerting: { - all: ['foo', 'bar'], - read: ['baz'], + rule: { + all: ['foo', 'bar'], + read: ['baz'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { + rule: { + read: ['foo', 'bar', 'baz'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents privileges from specifying alerting/alert entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['bar'], + privileges: { + all: { + alerting: { + alert: { + all: ['foo', 'bar'], + read: ['baz'], + }, }, savedObject: { all: [], @@ -849,7 +898,11 @@ describe('FeatureRegistry', () => { app: [], }, read: { - alerting: { read: ['foo', 'bar', 'baz'] }, + alerting: { + alert: { + read: ['foo', 'bar', 'baz'], + }, + }, savedObject: { all: [], read: [], @@ -869,7 +922,80 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { + it(`prevents features from specifying alerting/rule entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['foo', 'bar', 'baz'], + privileges: { + all: { + alerting: { + rule: { + all: ['foo'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + read: { + alerting: { + rule: { + all: ['foo'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + alerting: { + rule: { + all: ['bar'], + }, + }, + }, + ], + }, + ], + }, + ], + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + + it(`prevents features from specifying alerting/alert entries that don't exist at the privilege level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -878,7 +1004,11 @@ describe('FeatureRegistry', () => { alerting: ['foo', 'bar', 'baz'], privileges: { all: { - alerting: { all: ['foo'] }, + alerting: { + alert: { + all: ['foo'], + }, + }, savedObject: { all: [], read: [], @@ -887,7 +1017,11 @@ describe('FeatureRegistry', () => { app: [], }, read: { - alerting: { all: ['foo'] }, + alerting: { + alert: { + all: ['foo'], + }, + }, savedObject: { all: [], read: [], @@ -912,7 +1046,11 @@ describe('FeatureRegistry', () => { read: [], }, ui: [], - alerting: { all: ['bar'] }, + alerting: { + alert: { + all: ['bar'], + }, + }, }, ], }, @@ -930,7 +1068,47 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { + it(`prevents reserved privileges from specifying alerting/rule entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { + rule: { + all: ['foo', 'bar', 'baz'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` + ); + }); + + it(`prevents reserved privileges from specifying alerting/alert entries that don't exist at the root level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -944,7 +1122,11 @@ describe('FeatureRegistry', () => { { id: 'reserved', privilege: { - alerting: { all: ['foo', 'bar', 'baz'] }, + alerting: { + alert: { + all: ['foo', 'bar', 'baz'], + }, + }, savedObject: { all: [], read: [], @@ -966,7 +1148,47 @@ describe('FeatureRegistry', () => { ); }); - it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { + it(`prevents features from specifying alerting/rule entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + category: { id: 'foo', label: 'foo' }, + alerting: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { + rule: { + all: ['foo', 'bar'], + }, + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); + + it(`prevents features from specifying alerting/alert entries that don't exist at the reserved privilege level`, () => { const feature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', @@ -980,7 +1202,11 @@ describe('FeatureRegistry', () => { { id: 'reserved', privilege: { - alerting: { all: ['foo', 'bar'] }, + alerting: { + alert: { + all: ['foo', 'bar'], + }, + }, savedObject: { all: [], read: [], diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 51d3331ac7da15..00272efc8aa782 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -80,8 +80,18 @@ const kibanaPrivilegeSchema = schema.object({ app: schema.maybe(schema.arrayOf(schema.string())), alerting: schema.maybe( schema.object({ - all: schema.maybe(alertingSchema), - read: schema.maybe(alertingSchema), + rule: schema.maybe( + schema.object({ + all: schema.maybe(alertingSchema), + read: schema.maybe(alertingSchema), + }) + ), + alert: schema.maybe( + schema.object({ + all: schema.maybe(alertingSchema), + read: schema.maybe(alertingSchema), + }) + ), }) ), savedObject: schema.object({ @@ -106,8 +116,18 @@ const kibanaIndependentSubFeaturePrivilegeSchema = schema.object({ catalogue: schema.maybe(catalogueSchema), alerting: schema.maybe( schema.object({ - all: schema.maybe(alertingSchema), - read: schema.maybe(alertingSchema), + rule: schema.maybe( + schema.object({ + all: schema.maybe(alertingSchema), + read: schema.maybe(alertingSchema), + }) + ), + alert: schema.maybe( + schema.object({ + all: schema.maybe(alertingSchema), + read: schema.maybe(alertingSchema), + }) + ), }) ), api: schema.maybe(schema.arrayOf(schema.string())), @@ -274,8 +294,8 @@ export function validateKibanaFeature(feature: KibanaFeatureConfig) { } function validateAlertingEntry(privilegeId: string, entry: FeatureKibanaPrivileges['alerting']) { - const all = entry?.all ?? []; - const read = entry?.read ?? []; + const all: string[] = [...(entry?.rule?.all ?? []), ...(entry?.alert?.all ?? [])]; + const read: string[] = [...(entry?.rule?.read ?? []), ...(entry?.alert?.read ?? [])]; all.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); read.forEach((privilegeAlertTypes) => unseenAlertTypes.delete(privilegeAlertTypes)); diff --git a/x-pack/plugins/file_upload/public/api/index.ts b/x-pack/plugins/file_upload/public/api/index.ts index c2520547ddad9a..f3a184f27dfac8 100644 --- a/x-pack/plugins/file_upload/public/api/index.ts +++ b/x-pack/plugins/file_upload/public/api/index.ts @@ -5,16 +5,16 @@ * 2.0. */ -import React from 'react'; -import { FileUploadComponentProps, lazyLoadModules } from '../lazy_load_bundle'; +import { lazyLoadModules } from '../lazy_load_bundle'; import type { IImporter, ImportFactoryOptions } from '../importer'; -import { IndexNameFormProps } from '../'; import type { HasImportPermission, FindFileStructureResponse } from '../../common'; import type { getMaxBytes, getMaxBytesFormatted } from '../importer/get_max_bytes'; +import { JsonUploadAndParseAsyncWrapper } from './json_upload_and_parse_async_wrapper'; +import { IndexNameFormAsyncWrapper } from './index_name_form_async_wrapper'; export interface FileUploadStartApi { - getFileUploadComponent(): ReturnType; - getIndexNameFormComponent(): Promise>; + FileUploadComponent: typeof JsonUploadAndParseAsyncWrapper; + IndexNameFormComponent: typeof IndexNameFormAsyncWrapper; importerFactory: typeof importerFactory; getMaxBytes: typeof getMaxBytes; getMaxBytesFormatted: typeof getMaxBytesFormatted; @@ -30,19 +30,8 @@ export interface GetTimeFieldRangeResponse { end: { epoch: number; string: string }; } -export async function getFileUploadComponent(): Promise< - React.ComponentType -> { - const fileUploadModules = await lazyLoadModules(); - return fileUploadModules.JsonUploadAndParse; -} - -export async function getIndexNameFormComponent(): Promise< - React.ComponentType -> { - const fileUploadModules = await lazyLoadModules(); - return fileUploadModules.IndexNameForm; -} +export const FileUploadComponent = JsonUploadAndParseAsyncWrapper; +export const IndexNameFormComponent = IndexNameFormAsyncWrapper; export async function importerFactory( format: string, diff --git a/x-pack/plugins/file_upload/public/api/index_name_form_async_wrapper.tsx b/x-pack/plugins/file_upload/public/api/index_name_form_async_wrapper.tsx new file mode 100644 index 00000000000000..382e2eedebe24e --- /dev/null +++ b/x-pack/plugins/file_upload/public/api/index_name_form_async_wrapper.tsx @@ -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 React from 'react'; +import { EuiLoadingContent } from '@elastic/eui'; +import { lazyLoadModules } from '../lazy_load_bundle'; +import { IndexNameFormProps } from '../index'; + +interface State { + IndexNameForm: React.ComponentType | null; +} + +export class IndexNameFormAsyncWrapper extends React.Component { + state: State = { + IndexNameForm: null, + }; + + private _isMounted = false; + + componentWillUnmount(): void { + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + lazyLoadModules().then((modules) => { + if (this._isMounted) { + this.setState({ + IndexNameForm: modules.IndexNameForm, + }); + } + }); + } + + render() { + const { IndexNameForm } = this.state; + return IndexNameForm ? : ; + } +} diff --git a/x-pack/plugins/file_upload/public/api/json_upload_and_parse_async_wrapper.tsx b/x-pack/plugins/file_upload/public/api/json_upload_and_parse_async_wrapper.tsx new file mode 100644 index 00000000000000..281ea93b766947 --- /dev/null +++ b/x-pack/plugins/file_upload/public/api/json_upload_and_parse_async_wrapper.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiLoadingContent } from '@elastic/eui'; +import { FileUploadComponentProps, lazyLoadModules } from '../lazy_load_bundle'; + +interface State { + JsonUploadAndParse: React.ComponentType | null; +} + +export class JsonUploadAndParseAsyncWrapper extends React.Component< + FileUploadComponentProps, + State +> { + state: State = { + JsonUploadAndParse: null, + }; + private _isMounted = false; + + componentDidMount() { + this._isMounted = true; + lazyLoadModules().then((modules) => { + if (this._isMounted) { + this.setState({ + JsonUploadAndParse: modules.JsonUploadAndParse, + }); + } + }); + } + + componentWillUnmount(): void { + this._isMounted = false; + } + + render() { + const { JsonUploadAndParse } = this.state; + return JsonUploadAndParse ? ( + + ) : ( + + ); + } +} diff --git a/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts b/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts index b0f1b98a9ae728..9c7c6ff1e5180f 100644 --- a/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts @@ -32,7 +32,7 @@ export interface FileUploadComponentProps { let loadModulesPromise: Promise; -interface LazyLoadedFileUploadModules { +export interface LazyLoadedFileUploadModules { JsonUploadAndParse: React.ComponentType; IndexNameForm: React.ComponentType; importerFactory: (format: string, options: ImportFactoryOptions) => IImporter | undefined; diff --git a/x-pack/plugins/file_upload/public/plugin.ts b/x-pack/plugins/file_upload/public/plugin.ts index 6240dbe39a85e4..b7b81e19b22195 100644 --- a/x-pack/plugins/file_upload/public/plugin.ts +++ b/x-pack/plugins/file_upload/public/plugin.ts @@ -8,10 +8,10 @@ import { CoreStart, Plugin } from '../../../../src/core/public'; import { FileUploadStartApi, - getFileUploadComponent, + FileUploadComponent, importerFactory, hasImportPermission, - getIndexNameFormComponent, + IndexNameFormComponent, checkIndexExists, getTimeFieldRange, analyzeFile, @@ -42,8 +42,8 @@ export class FileUploadPlugin public start(core: CoreStart, plugins: FileUploadStartDependencies): FileUploadStartApi { setStartServices(core, plugins); return { - getFileUploadComponent, - getIndexNameFormComponent, + FileUploadComponent, + IndexNameFormComponent, importerFactory, getMaxBytes, getMaxBytesFormatted, diff --git a/x-pack/plugins/fleet/common/services/index.ts b/x-pack/plugins/fleet/common/services/index.ts index cee34db4c7ec4d..1fea5033e645c0 100644 --- a/x-pack/plugins/fleet/common/services/index.ts +++ b/x-pack/plugins/fleet/common/services/index.ts @@ -16,3 +16,4 @@ export { isValidNamespace } from './is_valid_namespace'; export { isDiffPathProtocol } from './is_diff_path_protocol'; export { LicenseService } from './license'; export { isAgentUpgradeable } from './is_agent_upgradeable'; +export { doesPackageHaveIntegrations } from './packages_with_integrations'; diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts index 0dfeb63f3b261b..8f79e633eed0c2 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.ts @@ -40,6 +40,21 @@ const getStreamsForInputType = ( return streams; }; +// Reduces registry var def into config object entry +const varsReducer = ( + configObject: PackagePolicyConfigRecord, + registryVar: RegistryVarsEntry +): PackagePolicyConfigRecord => { + const configEntry: PackagePolicyConfigRecordEntry = { + value: !registryVar.default && registryVar.multi ? [] : registryVar.default, + }; + if (registryVar.type) { + configEntry.type = registryVar.type; + } + configObject![registryVar.name] = configEntry; + return configObject; +}; + /* * This service creates a package policy inputs definition from defaults provided in package info */ @@ -58,21 +73,6 @@ export const packageToPackagePolicyInputs = ( if (packagePolicyTemplate?.inputs?.length) { // Map each package package policy input to agent policy package policy input packagePolicyTemplate.inputs.forEach((packageInput) => { - // Reduces registry var def into config object entry - const varsReducer = ( - configObject: PackagePolicyConfigRecord, - registryVar: RegistryVarsEntry - ): PackagePolicyConfigRecord => { - const configEntry: PackagePolicyConfigRecordEntry = { - value: !registryVar.default && registryVar.multi ? [] : registryVar.default, - }; - if (registryVar.type) { - configEntry.type = registryVar.type; - } - configObject![registryVar.name] = configEntry; - return configObject; - }; - // Map each package input stream into package policy input stream const streams: NewPackagePolicyInputStream[] = getStreamsForInputType( packageInput.type, @@ -121,7 +121,7 @@ export const packageToPackagePolicy = ( packagePolicyName?: string, description?: string ): NewPackagePolicy => { - return { + const packagePolicy: NewPackagePolicy = { name: packagePolicyName || `${packageInfo.name}-1`, namespace, description, @@ -135,4 +135,10 @@ export const packageToPackagePolicy = ( output_id: outputId, inputs: packageToPackagePolicyInputs(packageInfo), }; + + if (packageInfo.vars?.length) { + packagePolicy.vars = packageInfo.vars.reduce(varsReducer, {}); + } + + return packagePolicy; }; diff --git a/x-pack/plugins/beats_management/common/constants/plugin.ts b/x-pack/plugins/fleet/common/services/packages_with_integrations.ts similarity index 55% rename from x-pack/plugins/beats_management/common/constants/plugin.ts rename to x-pack/plugins/fleet/common/services/packages_with_integrations.ts index 912bc75b98f601..d63c7518f4edb9 100644 --- a/x-pack/plugins/beats_management/common/constants/plugin.ts +++ b/x-pack/plugins/fleet/common/services/packages_with_integrations.ts @@ -4,9 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import type { PackageInfo, PackageListItem } from '../types'; -export const PLUGIN = { - ID: 'beats_management', +export const doesPackageHaveIntegrations = (pkgInfo: PackageInfo | PackageListItem) => { + return (pkgInfo.policy_templates || []).length > 1; }; -export const CONFIG_PREFIX = 'xpack.beats'; -export const MANAGEMENT_SECTION = 'beats_management'; diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index eab13fe5819f97..5551453b8975c2 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -19,7 +19,12 @@ import type { } from '../../constants'; import type { ValueOf } from '../../types'; -import type { PackageSpecManifest, PackageSpecScreenshot } from './package_spec'; +import type { + PackageSpecManifest, + PackageSpecIcon, + PackageSpecScreenshot, + PackageSpecCategory, +} from './package_spec'; export type InstallationStatus = typeof installationStatuses; @@ -118,19 +123,20 @@ interface RegistryOverridePropertyValue { } export type RegistryRelease = PackageSpecManifest['release']; -export interface RegistryImage { - src: string; +export interface RegistryImage extends PackageSpecIcon { path: string; - title?: string; - size?: string; - type?: string; } export enum RegistryPolicyTemplateKeys { name = 'name', title = 'title', description = 'description', + icons = 'icons', + screenshots = 'screenshots', + categories = 'categories', + data_streams = 'data_streams', inputs = 'inputs', + readme = 'readme', multiple = 'multiple', } @@ -138,7 +144,12 @@ export interface RegistryPolicyTemplate { [RegistryPolicyTemplateKeys.name]: string; [RegistryPolicyTemplateKeys.title]: string; [RegistryPolicyTemplateKeys.description]: string; + [RegistryPolicyTemplateKeys.icons]?: RegistryImage[]; + [RegistryPolicyTemplateKeys.screenshots]?: RegistryImage[]; + [RegistryPolicyTemplateKeys.categories]?: Array; + [RegistryPolicyTemplateKeys.data_streams]?: string[]; [RegistryPolicyTemplateKeys.inputs]?: RegistryInput[]; + [RegistryPolicyTemplateKeys.readme]?: string; [RegistryPolicyTemplateKeys.multiple]?: boolean; } @@ -148,15 +159,19 @@ export enum RegistryInputKeys { description = 'description', template_path = 'template_path', condition = 'condition', + input_group = 'input_group', vars = 'vars', } +export type RegistryInputGroup = 'logs' | 'metrics'; + export interface RegistryInput { [RegistryInputKeys.type]: string; [RegistryInputKeys.title]: string; [RegistryInputKeys.description]: string; [RegistryInputKeys.template_path]?: string; [RegistryInputKeys.condition]?: string; + [RegistryInputKeys.input_group]?: RegistryInputGroup; [RegistryInputKeys.vars]?: RegistryVarsEntry[]; } @@ -273,7 +288,7 @@ export interface RegistryDataStream { [RegistryDataStreamKeys.streams]?: RegistryStream[]; [RegistryDataStreamKeys.package]: string; [RegistryDataStreamKeys.path]: string; - [RegistryDataStreamKeys.ingest_pipeline]: string; + [RegistryDataStreamKeys.ingest_pipeline]?: string; [RegistryDataStreamKeys.elasticsearch]?: RegistryElasticsearch; [RegistryDataStreamKeys.dataset_is_prefix]?: boolean; } @@ -307,7 +322,7 @@ export interface RegistryVarsEntry { [RegistryVarsEntryKeys.required]?: boolean; [RegistryVarsEntryKeys.show_user]?: boolean; [RegistryVarsEntryKeys.multi]?: boolean; - [RegistryVarsEntryKeys.default]?: string | string[]; + [RegistryVarsEntryKeys.default]?: string | string[] | boolean; [RegistryVarsEntryKeys.os]?: { [key: string]: { default: string | string[]; @@ -329,8 +344,11 @@ type Merge = Omit & { + integration?: string; + id: string; +}; -export type PackageListItem = Installable; export type PackagesGroupedByStatus = Record, PackageList>; export type PackageInfo = | Installable> diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index 04362e6ff9402f..c0b74c2a7b025f 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -58,6 +58,7 @@ export interface NewPackagePolicy { output_id: string; package?: PackagePolicyPackage; inputs: NewPackagePolicyInput[]; + vars?: PackagePolicyConfigRecord; } export interface UpdatePackagePolicy extends NewPackagePolicy { diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts index 65be72cbb7b6b3..57aaa0230e647b 100644 --- a/x-pack/plugins/fleet/common/types/models/package_spec.ts +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { RegistryPolicyTemplate } from './epm'; +import type { RegistryPolicyTemplate, RegistryVarsEntry } from './epm'; // Based on https://github.com/elastic/package-spec/blob/master/versions/1/manifest.spec.yml#L8 export interface PackageSpecManifest { @@ -22,6 +22,7 @@ export interface PackageSpecManifest { icons?: PackageSpecIcon[]; screenshots?: PackageSpecScreenshot[]; policy_templates?: RegistryPolicyTemplate[]; + vars?: RegistryVarsEntry[]; owner: { github: string }; } diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index e5c7ace420c730..51772eadca69e8 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -8,8 +8,7 @@ import type { AssetReference, CategorySummaryList, - Installable, - RegistrySearchResult, + PackageList, PackageInfo, PackageUsageStats, InstallType, @@ -18,6 +17,7 @@ import type { export interface GetCategoriesRequest { query: { experimental?: boolean; + include_policy_templates?: boolean; }; } @@ -33,7 +33,7 @@ export interface GetPackagesRequest { } export interface GetPackagesResponse { - response: Array>; + response: PackageList; } export interface GetLimitedPackagesResponse { diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/package_icon.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/package_icon.tsx index cb0b02527f756a..bb84a790569784 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/package_icon.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/package_icon.tsx @@ -14,7 +14,7 @@ import { usePackageIconType } from '../hooks'; export const PackageIcon: React.FunctionComponent< UsePackageIconType & Omit -> = ({ packageName, version, icons, tryApi, ...euiIconProps }) => { - const iconType = usePackageIconType({ packageName, version, icons, tryApi }); +> = ({ packageName, integrationName, version, icons, tryApi, ...euiIconProps }) => { + const iconType = usePackageIconType({ packageName, integrationName, version, icons, tryApi }); return ; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx index d741874a7504c5..b8028547910097 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/components/settings_flyout/index.tsx @@ -277,7 +277,7 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { values={{ link: ( @@ -301,9 +301,26 @@ export const SettingFlyout: React.FunctionComponent = ({ onClose }) => { label={i18n.translate('xpack.fleet.settings.elasticsearchUrlLabel', { defaultMessage: 'Elasticsearch hosts', })} - helpText={i18n.translate('xpack.fleet.settings.elasticsearchUrlsHelpTect', { - defaultMessage: 'Specify the Elasticsearch URLs where agents send data.', - })} + helpText={ + + + + ), + }} + /> + } {...inputs.elasticsearchUrl.formRowProps} > diff --git a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts index bcb450d5ec94e9..27df7a4ebf11d3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/constants/page_paths.ts @@ -56,7 +56,7 @@ export const PAGE_ROUTING_PATHS = { policy_details: '/policies/:policyId/:tabId?', policy_details_settings: '/policies/:policyId/settings', add_integration_from_policy: '/policies/:policyId/add-integration', - add_integration_to_policy: '/integrations/:pkgkey/add-integration', + add_integration_to_policy: '/integrations/:pkgkey/add-integration/:integration?', edit_integration: '/policies/:policyId/edit-integration/:packagePolicyId', fleet: '/fleet', fleet_agent_list: '/fleet/agents', @@ -77,17 +77,22 @@ export const pagePathGetters: { integrations: () => '/integrations', integrations_all: () => '/integrations', integrations_installed: () => '/integrations/installed', - integration_details_overview: ({ pkgkey }) => `/integrations/detail/${pkgkey}/overview`, - integration_details_policies: ({ pkgkey }) => `/integrations/detail/${pkgkey}/policies`, - integration_details_settings: ({ pkgkey }) => `/integrations/detail/${pkgkey}/settings`, - integration_details_custom: ({ pkgkey }) => `/integrations/detail/${pkgkey}/custom`, + integration_details_overview: ({ pkgkey, integration }) => + `/integrations/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`, + integration_details_policies: ({ pkgkey, integration }) => + `/integrations/detail/${pkgkey}/policies${integration ? `?integration=${integration}` : ''}`, + integration_details_settings: ({ pkgkey, integration }) => + `/integrations/detail/${pkgkey}/settings${integration ? `?integration=${integration}` : ''}`, + integration_details_custom: ({ pkgkey, integration }) => + `/integrations/detail/${pkgkey}/custom${integration ? `?integration=${integration}` : ''}`, integration_policy_edit: ({ packagePolicyId }) => `/integrations/edit-integration/${packagePolicyId}`, policies: () => '/policies', policies_list: () => '/policies', policy_details: ({ policyId, tabId }) => `/policies/${policyId}${tabId ? `/${tabId}` : ''}`, add_integration_from_policy: ({ policyId }) => `/policies/${policyId}/add-integration`, - add_integration_to_policy: ({ pkgkey }) => `/integrations/${pkgkey}/add-integration`, + add_integration_to_policy: ({ pkgkey, integration }) => + `/integrations/${pkgkey}/add-integration${integration ? `/${integration}` : ''}`, edit_integration: ({ policyId, packagePolicyId }) => `/policies/${policyId}/edit-integration/${packagePolicyId}`, fleet: () => '/fleet', diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_package_icon_type.ts b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_package_icon_type.ts index 654cfc70ab418b..5701dd100bfed9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_package_icon_type.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_package_icon_type.ts @@ -16,7 +16,8 @@ import { sendGetPackageInfoByKey } from './index'; type Package = PackageInfo | PackageListItem; export interface UsePackageIconType { - packageName: Package['name']; + packageName: string; + integrationName?: string; version: Package['version']; icons?: Package['icons']; tryApi?: boolean; // should it call API to try to find missing icons? @@ -26,6 +27,7 @@ const CACHED_ICONS = new Map(); export const usePackageIconType = ({ packageName, + integrationName, version, icons: paramIcons, tryApi = false, @@ -33,13 +35,13 @@ export const usePackageIconType = ({ const { toPackageImage } = useLinks(); const [iconList, setIconList] = useState(); const [iconType, setIconType] = useState(''); // FIXME: use `empty` icon during initialization - see: https://github.com/elastic/kibana/issues/60622 - const pkgKey = `${packageName}-${version}`; + const cacheKey = `${packageName}-${version}${integrationName ? `-${integrationName}` : ''}`; // Generates an icon path or Eui Icon name based on an icon list from the package // or by using the package name against logo icons from Eui useEffect(() => { - if (CACHED_ICONS.has(pkgKey)) { - setIconType(CACHED_ICONS.get(pkgKey) || ''); + if (CACHED_ICONS.has(cacheKey)) { + setIconType(CACHED_ICONS.get(cacheKey) || ''); return; } const svgIcons = (paramIcons || iconList)?.filter( @@ -48,29 +50,29 @@ export const usePackageIconType = ({ const localIconSrc = Array.isArray(svgIcons) && toPackageImage(svgIcons[0], packageName, version); if (localIconSrc) { - CACHED_ICONS.set(pkgKey, localIconSrc); - setIconType(CACHED_ICONS.get(pkgKey) || ''); + CACHED_ICONS.set(cacheKey, localIconSrc); + setIconType(CACHED_ICONS.get(cacheKey) || ''); return; } const euiLogoIcon = ICON_TYPES.find((key) => key.toLowerCase() === `logo${packageName}`); if (euiLogoIcon) { - CACHED_ICONS.set(pkgKey, euiLogoIcon); + CACHED_ICONS.set(cacheKey, euiLogoIcon); setIconType(euiLogoIcon); return; } if (tryApi && !paramIcons && !iconList) { - sendGetPackageInfoByKey(pkgKey) + sendGetPackageInfoByKey(cacheKey) .catch((error) => undefined) // Ignore API errors .then((res) => { - CACHED_ICONS.delete(pkgKey); + CACHED_ICONS.delete(cacheKey); setIconList(res?.data?.response?.icons); }); } - CACHED_ICONS.set(pkgKey, 'package'); + CACHED_ICONS.set(cacheKey, 'package'); setIconType('package'); - }, [paramIcons, pkgKey, toPackageImage, iconList, packageName, iconType, tryApi, version]); + }, [paramIcons, cacheKey, toPackageImage, iconList, packageName, iconType, tryApi, version]); return iconType; }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx index f312220d9faec5..55861726af4661 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/index.tsx @@ -333,6 +333,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { packagePolicy={packagePolicy} updatePackagePolicy={updatePackagePolicy} validationResults={validationResults!} + submitAttempted={formState === 'INVALID'} /> {/* Only show the out-of-box configuration step if a UI extension is NOT registered */} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts index e36a4b46039f4d..de1b8df9f95974 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/validate_package_policy.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { safeLoad } from 'js-yaml'; +import { keyBy } from 'lodash'; import { getFlattenedObject, isValidNamespace } from '../../../../services'; import type { @@ -32,12 +33,12 @@ export type PackagePolicyInputValidationResults = PackagePolicyConfigValidationR streams?: Record; }; -export interface PackagePolicyValidationResults { +export type PackagePolicyValidationResults = { name: Errors; description: Errors; namespace: Errors; inputs: Record | null; -} +} & PackagePolicyConfigValidationResults; /* * Returns validation information for a given package policy and package info @@ -67,6 +68,16 @@ export const validatePackagePolicy = ( validationResults.namespace = [namespaceValidation.error]; } + // Validate package-level vars + const packageVarsByName = keyBy(packageInfo.vars || [], 'name'); + const packageVars = Object.entries(packagePolicy.vars || {}); + if (packageVars.length) { + validationResults.vars = packageVars.reduce((results, [name, varEntry]) => { + results[name] = validatePackagePolicyConfig(varEntry, packageVarsByName[name]); + return results; + }, {} as ValidationEntry); + } + if ( !packageInfo.policy_templates || packageInfo.policy_templates.length === 0 || diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx index 87c5af56f31b26..e64598e583d353 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_configure_package.tsx @@ -25,7 +25,6 @@ import { Loading } from '../../../components'; import type { PackagePolicyValidationResults } from './services'; import { PackagePolicyInputPanel } from './components'; -import type { CreatePackagePolicyFrom } from './types'; const findStreamsForInputType = ( inputType: string, @@ -50,22 +49,12 @@ const findStreamsForInputType = ( }; export const StepConfigurePackagePolicy: React.FunctionComponent<{ - from?: CreatePackagePolicyFrom; packageInfo: PackageInfo; packagePolicy: NewPackagePolicy; - packagePolicyId?: string; updatePackagePolicy: (fields: Partial) => void; validationResults: PackagePolicyValidationResults; submitAttempted: boolean; -}> = ({ - from = 'policy', - packageInfo, - packagePolicy, - packagePolicyId, - updatePackagePolicy, - validationResults, - submitAttempted, -}) => { +}> = ({ packageInfo, packagePolicy, updatePackagePolicy, validationResults, submitAttempted }) => { // Configure inputs (and their streams) // Assume packages only export one config template for now const renderConfigureInputs = () => diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx index c1626d2b141212..26a31a1394f789 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/step_define_package_policy.tsx @@ -5,14 +5,13 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiFieldText, EuiButtonEmpty, - EuiSpacer, EuiText, EuiComboBox, EuiDescribedFormGroup, @@ -21,180 +20,145 @@ import { EuiLink, } from '@elastic/eui'; -import type { AgentPolicy, PackageInfo, PackagePolicy, NewPackagePolicy } from '../../../types'; -import { packageToPackagePolicyInputs } from '../../../services'; +import type { + AgentPolicy, + PackageInfo, + PackagePolicy, + NewPackagePolicy, + RegistryVarsEntry, +} from '../../../types'; +import { packageToPackagePolicy } from '../../../services'; import { Loading } from '../../../components'; import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; +import { isAdvancedVar } from './services'; import type { PackagePolicyValidationResults } from './services'; +import { PackagePolicyInputVarField } from './components'; export const StepDefinePackagePolicy: React.FunctionComponent<{ agentPolicy: AgentPolicy; packageInfo: PackageInfo; packagePolicy: NewPackagePolicy; + integration?: string; updatePackagePolicy: (fields: Partial) => void; validationResults: PackagePolicyValidationResults; -}> = ({ agentPolicy, packageInfo, packagePolicy, updatePackagePolicy, validationResults }) => { - // Form show/hide states - const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); - - // Update package policy's package and agent policy info - useEffect(() => { - const pkg = packagePolicy.package; - const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : ''; - const pkgKey = pkgKeyFromPackageInfo(packageInfo); + submitAttempted: boolean; +}> = memo( + ({ + agentPolicy, + packageInfo, + packagePolicy, + integration, + updatePackagePolicy, + validationResults, + submitAttempted, + }) => { + // Form show/hide states + const [isShowingAdvanced, setIsShowingAdvanced] = useState(false); - // If package has changed, create shell package policy with input&stream values based on package info - if (currentPkgKey !== pkgKey) { - // Existing package policies on the agent policy using the package name, retrieve highest number appended to package policy name - const pkgPoliciesNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); - const pkgPoliciesWithMatchingNames = (agentPolicy.package_policies as PackagePolicy[]) - .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) - .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)) - .sort((a, b) => a - b); + // Package-level vars + const requiredVars: RegistryVarsEntry[] = []; + const advancedVars: RegistryVarsEntry[] = []; - updatePackagePolicy({ - // FIXME: Improve package policies name uniqueness - https://github.com/elastic/kibana/issues/72948 - name: `${packageInfo.name}-${ - pkgPoliciesWithMatchingNames.length - ? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1 - : 1 - }`, - package: { - name: packageInfo.name, - title: packageInfo.title, - version: packageInfo.version, - }, - inputs: packageToPackagePolicyInputs(packageInfo), + if (packageInfo.vars) { + packageInfo.vars.forEach((varDef) => { + if (isAdvancedVar(varDef)) { + advancedVars.push(varDef); + } else { + requiredVars.push(varDef); + } }); } - // If agent policy has changed, update package policy's agent policy ID and namespace - if (packagePolicy.policy_id !== agentPolicy.id) { - updatePackagePolicy({ - policy_id: agentPolicy.id, - namespace: agentPolicy.namespace, - }); - } - }, [ - packagePolicy.package, - packagePolicy.policy_id, - agentPolicy, - packageInfo, - updatePackagePolicy, - ]); + // Update package policy's package and agent policy info + useEffect(() => { + const pkg = packagePolicy.package; + const currentPkgKey = pkg ? pkgKeyFromPackageInfo(pkg) : ''; + const pkgKey = pkgKeyFromPackageInfo(packageInfo); - return validationResults ? ( - - - + // If package has changed, create shell package policy with input&stream values based on package info + if (currentPkgKey !== pkgKey) { + // Existing package policies on the agent policy using the package name, retrieve highest number appended to package policy name + const pkgPoliciesNamePattern = new RegExp(`${packageInfo.name}-(\\d+)`); + const pkgPoliciesWithMatchingNames = (agentPolicy.package_policies as PackagePolicy[]) + .filter((ds) => Boolean(ds.name.match(pkgPoliciesNamePattern))) + .map((ds) => parseInt(ds.name.match(pkgPoliciesNamePattern)![1], 10)) + .sort((a, b) => a - b); + + updatePackagePolicy( + packageToPackagePolicy( + packageInfo, + agentPolicy.id, + packagePolicy.output_id, + packagePolicy.namespace, + `${packageInfo.name}-${ + pkgPoliciesWithMatchingNames.length + ? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1 + : 1 + }`, + packagePolicy.description + ) + ); } - description={ - + + // If agent policy has changed, update package policy's agent policy ID and namespace + if (packagePolicy.policy_id !== agentPolicy.id) { + updatePackagePolicy({ + policy_id: agentPolicy.id, + namespace: agentPolicy.namespace, + }); } - > - <> - {/* Name */} - - } - > - - updatePackagePolicy({ - name: e.target.value, - }) - } - data-test-subj="packagePolicyNameInput" - /> - + }, [packagePolicy, agentPolicy, packageInfo, updatePackagePolicy, integration]); - {/* Description */} - - } - labelAppend={ - - - - } - isInvalid={!!validationResults.description} - error={validationResults.description} - > - - updatePackagePolicy({ - description: e.target.value, - }) - } - data-test-subj="packagePolicyDescriptionInput" + + } + description={ + - - - - {/* Advanced options toggle */} - - - setIsShowingAdvanced(!isShowingAdvanced)} - flush="left" + } + > + + {/* Name */} + + + } > - + updatePackagePolicy({ + name: e.target.value, + }) + } + data-test-subj="packagePolicyNameInput" /> - + - {!isShowingAdvanced && !!validationResults.namespace ? ( - - - - - - ) : null} - - {/* Advanced options content */} - {/* Todo: Populate list of existing namespaces */} - {isShowingAdvanced ? ( - <> - + {/* Description */} + } helpText={ @@ -216,30 +180,156 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{ }} /> } + labelAppend={ + + + + } + isInvalid={!!validationResults.description} + error={validationResults.description} > - { - updatePackagePolicy({ - namespace: newNamespace, - }); - }} - onChange={(newNamespaces: Array<{ label: string }>) => { + updatePackagePolicy({ - namespace: newNamespaces.length ? newNamespaces[0].label : '', - }); - }} + description: e.target.value, + }) + } + data-test-subj="packagePolicyDescriptionInput" /> - - ) : null} - - - ) : ( - - ); -}; + + + {/* Required vars */} + {requiredVars.map((varDef) => { + const { name: varName, type: varType } = varDef; + if (!packagePolicy.vars || !packagePolicy.vars[varName]) return null; + const value = packagePolicy.vars[varName].value; + return ( + + { + updatePackagePolicy({ + vars: { + ...packagePolicy.vars, + [varName]: { + type: varType, + value: newValue, + }, + }, + }); + }} + errors={validationResults.vars![varName]} + forceShowErrors={submitAttempted} + /> + + ); + })} + + {/* Advanced options toggle */} + + + + setIsShowingAdvanced(!isShowingAdvanced)} + flush="left" + > + + + + {!isShowingAdvanced && !!validationResults.namespace ? ( + + + + + + ) : null} + + + + {/* Advanced options content */} + {/* Todo: Populate list of existing namespaces */} + {isShowingAdvanced ? ( + + + + + } + > + { + updatePackagePolicy({ + namespace: newNamespace, + }); + }} + onChange={(newNamespaces: Array<{ label: string }>) => { + updatePackagePolicy({ + namespace: newNamespaces.length ? newNamespaces[0].label : '', + }); + }} + /> + + + {/* Advanced vars */} + {advancedVars.map((varDef) => { + const { name: varName, type: varType } = varDef; + if (!packagePolicy.vars || !packagePolicy.vars[varName]) return null; + const value = packagePolicy.vars![varName].value; + return ( + + { + updatePackagePolicy({ + vars: { + ...packagePolicy.vars, + [varName]: { + type: varType, + value: newValue, + }, + }, + }); + }} + errors={validationResults.vars![varName]} + forceShowErrors={submitAttempted} + /> + + ); + })} + + + ) : null} + + + ) : ( + + ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 8df8b7ebcd4cfb..3b9b5696ff7bdc 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -351,15 +351,14 @@ export const EditPackagePolicyForm = memo<{ packagePolicy={packagePolicy} updatePackagePolicy={updatePackagePolicy} validationResults={validationResults!} + submitAttempted={formState === 'INVALID'} /> {/* Only show the out-of-box configuration step if a UI extension is NOT registered */} {!ExtensionView && ( } - href={getHref('integration_details_overview', { pkgkey: `${name}-${urlVersion}` })} + icon={ + + } + href={getHref('integration_details_overview', { + pkgkey: `${name}-${urlVersion}`, + ...(integration ? { integration } : {}), + })} betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined} betaBadgeTooltipContent={ release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx index b79255e6889ada..d4d2b481950f6b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/package_list_grid.tsx @@ -25,7 +25,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { Loading } from '../../../components'; import type { PackageList } from '../../../types'; import { useLocalSearch, searchIdField } from '../hooks'; -import { pkgKeyFromPackageInfo } from '../../../services/pkg_key_from_package_info'; import { PackageCard } from './package_card'; @@ -153,11 +152,13 @@ function GridColumn({ list, showMissingIntegrationMessage = false }: GridColumnP return ( {list.length ? ( - list.map((item) => ( - - - - )) + list.map((item) => { + return ( + + + + ); + }) ) : ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_local_search.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_local_search.tsx index efdc2f3884542f..43db5657b06155 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_local_search.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_local_search.tsx @@ -8,11 +8,10 @@ import { Search as LocalSearch } from 'js-search'; import { useEffect, useRef } from 'react'; -import type { PackageList, PackageListItem } from '../../../types'; +import type { PackageList } from '../../../types'; -export type SearchField = keyof PackageListItem; -export const searchIdField: SearchField = 'name'; -export const fieldsToSearch: SearchField[] = ['description', 'name', 'title']; +export const searchIdField = 'id'; +export const fieldsToSearch = ['description', 'name', 'title']; export function useLocalSearch(packageList: PackageList) { const localSearchRef = useRef(null); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx index 6d5d52789a9754..8a82397bd01932 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/components/icon_panel.tsx @@ -37,10 +37,11 @@ const Panel = styled(EuiPanel)` export function IconPanel({ packageName, + integrationName, version, icons, -}: Pick) { - const iconType = usePackageIconType({ packageName, version, icons }); +}: Pick) { + const iconType = usePackageIconType({ packageName, integrationName, version, icons }); return ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx index a4f465cd3d6195..05d2f53688655e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/index.tsx @@ -74,7 +74,9 @@ export function Detail() { const { getHref, getPath } = useLink(); const hasWriteCapabilites = useCapabilities().write; const history = useHistory(); - const location = useLocation(); + const { pathname, search, hash } = useLocation(); + const queryParams = useMemo(() => new URLSearchParams(search), [search]); + const integration = useMemo(() => queryParams.get('integration'), [queryParams]); // Package info state const [packageInfo, setPackageInfo] = useState(null); @@ -120,6 +122,16 @@ export function Detail() { } }, [packageInfoData, setPackageInstallStatus, setPackageInfo]); + const integrationInfo = useMemo( + () => + integration + ? packageInfo?.policy_templates?.find( + (policyTemplate) => policyTemplate.name === integration + ) + : undefined, + [integration, packageInfo] + ); + const headerLeftContent = useMemo( () => ( @@ -147,8 +159,9 @@ export function Detail() { ) : ( )} @@ -157,7 +170,7 @@ export function Detail() { {/* Render space in place of package name while package info loads to prevent layout from jumping around */} -

{packageInfo?.title || '\u00A0'}

+

{integrationInfo?.title || packageInfo?.title || '\u00A0'}

{packageInfo?.release && packageInfo.release !== 'ga' ? ( @@ -174,7 +187,7 @@ export function Detail() { ), - [getHref, isLoading, packageInfo] + [getHref, integrationInfo, isLoading, packageInfo] ); const handleAddIntegrationPolicyClick = useCallback( @@ -184,9 +197,9 @@ export function Detail() { // The object below, given to `createHref` is explicitly accessing keys of `location` in order // to ensure that dependencies to this `useCallback` is set correctly (because `location` is mutable) const currentPath = history.createHref({ - pathname: location.pathname, - search: location.search, - hash: location.hash, + pathname, + search, + hash, }); const redirectToPath: CreatePackagePolicyRouteState['onSaveNavigateTo'] & CreatePackagePolicyRouteState['onCancelNavigateTo'] = [ @@ -204,11 +217,12 @@ export function Detail() { history.push({ pathname: getPath('add_integration_to_policy', { pkgkey, + ...(integration ? { integration } : {}), }), state: redirectBackRouteState, }); }, - [getPath, history, location.hash, location.pathname, location.search, pkgkey] + [getPath, history, hash, pathname, search, pkgkey, integration] ); const headerRightContent = useMemo( @@ -255,6 +269,7 @@ export function Detail() { iconType="plusInCircle" href={getHref('add_integration_to_policy', { pkgkey, + ...(integration ? { integration } : {}), })} onClick={handleAddIntegrationPolicyClick} data-test-subj="addIntegrationPolicyButton" @@ -263,7 +278,7 @@ export function Detail() { id="xpack.fleet.epm.addPackagePolicyButtonText" defaultMessage="Add {packageName}" values={{ - packageName: packageInfo.title, + packageName: integrationInfo?.title || packageInfo.title, }} /> @@ -290,6 +305,8 @@ export function Detail() { getHref, handleAddIntegrationPolicyClick, hasWriteCapabilites, + integration, + integrationInfo, packageInfo, packageInstallStatus, pkgkey, @@ -316,6 +333,7 @@ export function Detail() { 'data-test-subj': `tab-overview`, href: getHref('integration_details_overview', { pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), }), }, ]; @@ -333,6 +351,7 @@ export function Detail() { 'data-test-subj': `tab-policies`, href: getHref('integration_details_policies', { pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), }), }); } @@ -349,6 +368,7 @@ export function Detail() { 'data-test-subj': `tab-settings`, href: getHref('integration_details_settings', { pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), }), }); @@ -365,12 +385,13 @@ export function Detail() { 'data-test-subj': `tab-custom`, href: getHref('integration_details_custom', { pkgkey: packageInfoKey, + ...(integration ? { integration } : {}), }), }); } return tabs; - }, [getHref, packageInfo, panel, showCustomTab, packageInstallStatus]); + }, [packageInfo, panel, getHref, integration, packageInstallStatus, showCustomTab]); return ( - {packageInfo ? : null} + {integrationInfo ? : null} {packageInfoError ? ( - + diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx index 3585e2063734b8..945859ac81ffdd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/overview/overview.tsx @@ -4,11 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import type { PackageInfo } from '../../../../../types'; +import type { PackageInfo, RegistryPolicyTemplate } from '../../../../../types'; import { Screenshots } from './screenshots'; import { Readme } from './readme'; @@ -16,6 +16,7 @@ import { Details } from './details'; interface Props { packageInfo: PackageInfo; + integrationInfo?: RegistryPolicyTemplate; } const LeftColumn = styled(EuiFlexItem)` @@ -25,14 +26,19 @@ const LeftColumn = styled(EuiFlexItem)` } `; -export const OverviewPage: React.FC = memo(({ packageInfo }: Props) => { +export const OverviewPage: React.FC = memo(({ packageInfo, integrationInfo }) => { + const screenshots = useMemo(() => integrationInfo?.screenshots || packageInfo.screenshots || [], [ + integrationInfo, + packageInfo.screenshots, + ]); + return ( {packageInfo.readme ? ( @@ -40,10 +46,10 @@ export const OverviewPage: React.FC = memo(({ packageInfo }: Props) => { - {packageInfo.screenshots && packageInfo.screenshots.length ? ( + {screenshots.length ? ( diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/index.tsx index e9bd6b7b61385a..922628e7b68b13 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/home/index.tsx @@ -5,22 +5,24 @@ * 2.0. */ -import React, { useState } from 'react'; +import React, { memo, useState, useMemo } from 'react'; import { useRouteMatch, Switch, Route, useLocation, useHistory } from 'react-router-dom'; +import semverLt from 'semver/functions/lt'; import type { Props as EuiTabProps } from '@elastic/eui/src/components/tabs/tab'; import { i18n } from '@kbn/i18n'; import { installationStatuses } from '../../../../../../../common/constants'; import { PAGE_ROUTING_PATHS } from '../../../../constants'; import { useLink, useGetCategories, useGetPackages, useBreadcrumbs } from '../../../../hooks'; +import { doesPackageHaveIntegrations } from '../../../../services'; import { WithHeaderLayout } from '../../../../layouts'; -import type { CategorySummaryItem } from '../../../../types'; +import type { CategorySummaryItem, PackageList } from '../../../../types'; import { PackageListGrid } from '../../components/package_list_grid'; import { CategoryFacets } from './category_facets'; import { HeroCopy, HeroImage } from './header'; -export function EPMHomePage() { +export const EPMHomePage: React.FC = memo(() => { const { params: { tabId }, } = useRouteMatch<{ tabId?: string }>(); @@ -61,51 +63,94 @@ export function EPMHomePage() { ); -} - -function InstalledPackages() { +}); + +// Packages can export multiple integrations, aka `policy_templates` +// In the case where packages ship >1 `policy_templates`, we flatten out the +// list of packages by bringing all integrations to top-level so that +// each integration is displayed as its own tile +const packageListToIntegrationsList = (packages: PackageList): PackageList => { + return packages.reduce((acc: PackageList, pkg) => { + const { policy_templates: policyTemplates = [], ...restOfPackage } = pkg; + return [ + ...acc, + restOfPackage, + ...(doesPackageHaveIntegrations(pkg) + ? policyTemplates.map((integration) => { + const { name, title, description, icons } = integration; + return { + ...restOfPackage, + id: `${restOfPackage}-${name}`, + integration: name, + title, + description, + icons: icons || restOfPackage.icons, + }; + }) + : []), + ]; + }, []); +}; + +const InstalledPackages: React.FC = memo(() => { useBreadcrumbs('integrations_installed'); const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({ experimental: true, }); const [selectedCategory, setSelectedCategory] = useState(''); - const title = i18n.translate('xpack.fleet.epmList.installedTitle', { - defaultMessage: 'Installed integrations', - }); - - const allInstalledPackages = - allPackages && allPackages.response - ? allPackages.response.filter((pkg) => pkg.status === installationStatuses.Installed) - : []; + const allInstalledPackages = useMemo( + () => + (allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed), + [allPackages?.response] + ); - const updatablePackages = allInstalledPackages.filter( - (item) => 'savedObject' in item && item.version > item.savedObject.attributes.version + const updatablePackages = useMemo( + () => + allInstalledPackages.filter( + (item) => + 'savedObject' in item && semverLt(item.savedObject.attributes.version, item.version) + ), + [allInstalledPackages] ); - const categories = [ - { - id: '', - title: i18n.translate('xpack.fleet.epmList.allFilterLinkText', { - defaultMessage: 'All', - }), - count: allInstalledPackages.length, - }, - { - id: 'updates_available', - title: i18n.translate('xpack.fleet.epmList.updatesAvailableFilterLinkText', { - defaultMessage: 'Updates available', + const title = useMemo( + () => + i18n.translate('xpack.fleet.epmList.installedTitle', { + defaultMessage: 'Installed integrations', }), - count: updatablePackages.length, - }, - ]; + [] + ); - const controls = ( - setSelectedCategory(id)} - /> + const categories = useMemo( + () => [ + { + id: '', + title: i18n.translate('xpack.fleet.epmList.allFilterLinkText', { + defaultMessage: 'All', + }), + count: allInstalledPackages.length, + }, + { + id: 'updates_available', + title: i18n.translate('xpack.fleet.epmList.updatesAvailableFilterLinkText', { + defaultMessage: 'Updates available', + }), + count: updatablePackages.length, + }, + ], + [allInstalledPackages.length, updatablePackages.length] + ); + + const controls = useMemo( + () => ( + setSelectedCategory(id)} + /> + ), + [categories, selectedCategory] ); return ( @@ -116,9 +161,9 @@ function InstalledPackages() { list={selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages} /> ); -} +}); -function AvailablePackages() { +const AvailablePackages: React.FC = memo(() => { useBreadcrumbs('integrations_all'); const history = useHistory(); const queryParams = new URLSearchParams(useLocation().search); @@ -128,24 +173,36 @@ function AvailablePackages() { const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({ category: selectedCategory, }); - const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories(); - const packages = - categoryPackagesRes && categoryPackagesRes.response ? categoryPackagesRes.response : []; - - const title = i18n.translate('xpack.fleet.epmList.allTitle', { - defaultMessage: 'Browse by category', + const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories({ + include_policy_templates: true, }); + const packages = useMemo( + () => packageListToIntegrationsList(categoryPackagesRes?.response || []), + [categoryPackagesRes] + ); - const categories = [ - { - id: '', - title: i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', { - defaultMessage: 'All', + const title = useMemo( + () => + i18n.translate('xpack.fleet.epmList.allTitle', { + defaultMessage: 'Browse by category', }), - count: allPackagesRes?.response?.length || 0, - }, - ...(categoriesRes ? categoriesRes.response : []), - ]; + [] + ); + + const categories = useMemo( + () => [ + { + id: '', + title: i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', { + defaultMessage: 'All', + }), + count: allPackagesRes?.response?.length || 0, + }, + ...(categoriesRes ? categoriesRes.response : []), + ], + [allPackagesRes?.response?.length, categoriesRes] + ); + const controls = categories ? ( ); -} +}); diff --git a/x-pack/plugins/fleet/public/applications/fleet/services/index.ts b/x-pack/plugins/fleet/public/applications/fleet/services/index.ts index 8d59496d8f9d0e..07fb04628ea2e5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/services/index.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/services/index.ts @@ -20,6 +20,7 @@ export { outputRoutesService, settingsRoutesService, appRoutesService, + packageToPackagePolicy, packageToPackagePolicyInputs, storedPackagePoliciesToAgentInputs, fullAgentPolicyToYaml, @@ -28,4 +29,5 @@ export { isValidNamespace, LicenseService, isAgentUpgradeable, + doesPackageHaveIntegrations, } from '../../../../common'; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 7f2b9d93e2df7c..5aa400d6443e6c 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -24,7 +24,9 @@ jest.mock('../../services/package_policy', (): { } => { return { packagePolicyService: { - compilePackagePolicyInputs: jest.fn((packageInfo, dataInputs) => Promise.resolve(dataInputs)), + compilePackagePolicyInputs: jest.fn((packageInfo, vars, dataInputs) => + Promise.resolve(dataInputs) + ), buildPackagePolicyFromPackage: jest.fn(), bulkCreate: jest.fn(), create: jest.fn((soClient, esClient, newData) => diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 4331baef11001d..10e5a6ac57f576 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -229,6 +229,7 @@ const getSavedObjectTypes = ( version: { type: 'keyword' }, }, }, + vars: { type: 'flattened' }, inputs: { type: 'nested', enabled: false, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts index 097f9ce28c7d1d..f915e871b98f7a 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/ingest_pipeline/ingest_pipelines.test.ts @@ -121,7 +121,7 @@ test('getPipelineNameForInstallation gets correct name', () => { const packageVersion = '1.0.1'; const pipelineRefName = 'pipeline-json'; const pipelineEntryNameForInstallation = getPipelineNameForInstallation({ - pipelineName: dataStream.ingest_pipeline, + pipelineName: dataStream.ingest_pipeline!, dataStream, packageVersion, }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 706b2679ed2eb9..e4e4f9c55fd2be 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -18,6 +18,7 @@ import type { ArchivePackage, RegistryPackage, EpmPackageAdditions, + GetCategoriesRequest, } from '../../../../common/types'; import type { Installation, PackageInfo } from '../../../types'; import { IngestManagerError } from '../../../errors'; @@ -35,7 +36,7 @@ function nameAsTitle(name: string) { return name.charAt(0).toUpperCase() + name.substr(1).toLowerCase(); } -export async function getCategories(options: Registry.CategoriesParams) { +export async function getCategories(options: GetCategoriesRequest['query']) { return Registry.fetchCategories(options); } @@ -47,7 +48,7 @@ export async function getPackages( const { savedObjectsClient, experimental, category } = options; const registryItems = await Registry.fetchList({ category, experimental }).then((items) => { return items.map((item) => - Object.assign({}, item, { title: item.title || nameAsTitle(item.name) }) + Object.assign({}, item, { title: item.title || nameAsTitle(item.name) }, { id: item.name }) ); }); // get the installed packages diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 9e04d75f36a16a..5ee7e198555c59 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -20,6 +20,7 @@ import type { RegistryPackage, RegistrySearchResults, RegistrySearchResult, + GetCategoriesRequest, } from '../../../types'; import { getArchiveFilelist, @@ -45,10 +46,6 @@ export interface SearchParams { experimental?: boolean; } -export interface CategoriesParams { - experimental?: boolean; -} - /** * Extract the package name and package version from a string. * @@ -150,13 +147,18 @@ function setKibanaVersion(url: URL) { } } -export async function fetchCategories(params?: CategoriesParams): Promise { +export async function fetchCategories( + params?: GetCategoriesRequest['query'] +): Promise { const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/categories`); if (params) { if (params.experimental) { url.searchParams.set('experimental', params.experimental.toString()); } + if (params.include_policy_templates) { + url.searchParams.set('include_policy_templates', params.include_policy_templates.toString()); + } } setKibanaVersion(url); diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index ab2bc944a1932f..46e5db3a958646 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -34,6 +34,12 @@ paths: {{#each paths}} - {{this}} {{/each}} +{{#if hosts}} +hosts: +{{#each hosts}} +- {{this}} +{{/each}} +{{/if}} `), }, ]; @@ -118,6 +124,7 @@ describe('Package policy service', () => { }, ], } as unknown) as PackageInfo, + {}, [ { type: 'log', @@ -180,6 +187,7 @@ describe('Package policy service', () => { }, ], } as unknown) as PackageInfo, + {}, [ { type: 'log', @@ -231,6 +239,74 @@ describe('Package policy service', () => { }, ], } as unknown) as PackageInfo, + {}, + [ + { + type: 'log', + enabled: true, + vars: { + paths: { + value: ['/var/log/set.log'], + }, + }, + streams: [ + { + id: 'datastream01', + data_stream: { dataset: 'package.dataset1', type: 'logs' }, + enabled: true, + }, + ], + }, + ] + ); + + expect(inputs).toEqual([ + { + type: 'log', + enabled: true, + vars: { + paths: { + value: ['/var/log/set.log'], + }, + }, + streams: [ + { + id: 'datastream01', + data_stream: { dataset: 'package.dataset1', type: 'logs' }, + enabled: true, + compiled_stream: { + metricset: ['dataset1'], + paths: ['/var/log/set.log'], + type: 'log', + }, + }, + ], + }, + ]); + }); + + it('should work with config variables at the package level', async () => { + const inputs = await packagePolicyService.compilePackagePolicyInputs( + ({ + data_streams: [ + { + dataset: 'package.dataset1', + type: 'logs', + streams: [{ input: 'log', template_path: 'some_template_path.yml' }], + path: 'dataset1', + }, + ], + policy_templates: [ + { + inputs: [{ type: 'log' }], + }, + ], + } as unknown) as PackageInfo, + { + hosts: { + value: ['localhost'], + }, + }, [ { type: 'log', @@ -269,6 +345,7 @@ describe('Package policy service', () => { metricset: ['dataset1'], paths: ['/var/log/set.log'], type: 'log', + hosts: ['localhost'], }, }, ], @@ -286,6 +363,7 @@ describe('Package policy service', () => { }, ], } as unknown) as PackageInfo, + {}, [ { type: 'log', @@ -334,6 +412,7 @@ describe('Package policy service', () => { }, ], } as unknown) as PackageInfo, + {}, [ { type: 'log', @@ -380,6 +459,7 @@ describe('Package policy service', () => { compiled_stream: { metricset: ['dataset1'], paths: ['/var/log/set.log'], + hosts: ['localhost'], type: 'log', }, }, @@ -397,6 +477,7 @@ describe('Package policy service', () => { }, ], } as unknown) as PackageInfo, + {}, [] ); @@ -412,6 +493,7 @@ describe('Package policy service', () => { }, ], } as unknown) as PackageInfo, + {}, [] ); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 7c870415bc521b..19ff55fbf2e732 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -128,7 +128,7 @@ class PackagePolicyService { } } - inputs = await this.compilePackagePolicyInputs(pkgInfo, inputs); + inputs = await this.compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs); } const isoDate = new Date().toISOString(); @@ -356,7 +356,7 @@ class PackagePolicyService { pkgVersion: packagePolicy.package.version, }); - inputs = await this.compilePackagePolicyInputs(pkgInfo, inputs); + inputs = await this.compilePackagePolicyInputs(pkgInfo, packagePolicy.vars || {}, inputs); } await soClient.update( @@ -432,7 +432,7 @@ class PackagePolicyService { ): Promise { const pkgInstall = await getInstallation({ savedObjectsClient: soClient, pkgName }); if (pkgInstall) { - const [pkgInfo, defaultOutputId] = await Promise.all([ + const [packageInfo, defaultOutputId] = await Promise.all([ getPackageInfo({ savedObjectsClient: soClient, pkgName: pkgInstall.name, @@ -440,23 +440,24 @@ class PackagePolicyService { }), outputService.getDefaultOutputId(soClient), ]); - if (pkgInfo) { + if (packageInfo) { if (!defaultOutputId) { throw new Error('Default output is not set'); } - return packageToPackagePolicy(pkgInfo, '', defaultOutputId); + return packageToPackagePolicy(packageInfo, '', defaultOutputId); } } } public async compilePackagePolicyInputs( pkgInfo: PackageInfo, + vars: PackagePolicy['vars'], inputs: PackagePolicyInput[] ): Promise { const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version); const inputsPromises = inputs.map(async (input) => { - const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, input); - const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, input); + const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, vars, input); + const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, vars, input); return { ...input, compiled_input: compiledInput, @@ -506,6 +507,7 @@ function assignStreamIdToInput(packagePolicyId: string, input: NewPackagePolicyI async function _compilePackagePolicyInput( registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, + vars: PackagePolicy['vars'], input: PackagePolicyInput ) { if ((!input.enabled || !pkgInfo.policy_templates?.[0]?.inputs?.length) ?? 0 > 0) { @@ -531,8 +533,8 @@ async function _compilePackagePolicyInput( } return compileTemplate( - // Populate template variables from input vars - Object.assign({}, input.vars), + // Populate template variables from package- and input-level vars + Object.assign({}, vars, input.vars), pkgInputTemplate.buffer.toString() ); } @@ -540,10 +542,11 @@ async function _compilePackagePolicyInput( async function _compilePackageStreams( registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, + vars: PackagePolicy['vars'], input: PackagePolicyInput ) { const streamsPromises = input.streams.map((stream) => - _compilePackageStream(registryPkgInfo, pkgInfo, input, stream) + _compilePackageStream(registryPkgInfo, pkgInfo, vars, input, stream) ); return await Promise.all(streamsPromises); @@ -552,6 +555,7 @@ async function _compilePackageStreams( async function _compilePackageStream( registryPkgInfo: RegistryPackage, pkgInfo: PackageInfo, + vars: PackagePolicy['vars'], input: PackagePolicyInput, stream: PackagePolicyInputStream ) { @@ -600,8 +604,8 @@ async function _compilePackageStream( } const yaml = compileTemplate( - // Populate template variables from input vars and stream vars - Object.assign({}, input.vars, stream.vars), + // Populate template variables from package-, input-, and stream-level vars + Object.assign({}, vars, input.vars, stream.vars), pkgStreamTemplate.buffer.toString() ); diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 886b4a26b833bb..a48a389ae689e7 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -71,6 +71,7 @@ export { InstallType, InstallSource, InstallResult, + GetCategoriesRequest, DataType, dataTypes, // Fleet Server types diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index fa467a4185bd40..cbf311cac4e3bf 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -77,6 +77,7 @@ const PackagePolicyBaseSchema = { ), }) ), + vars: schema.maybe(ConfigRecordSchema), }; export const NewPackagePolicySchema = schema.object({ diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index f7e3ed906e24b0..25f1e766a74763 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -10,6 +10,7 @@ import { schema } from '@kbn/config-schema'; export const GetCategoriesRequestSchema = { query: schema.object({ experimental: schema.maybe(schema.boolean()), + include_policy_templates: schema.maybe(schema.boolean()), }), }; diff --git a/x-pack/plugins/infra/server/features.ts b/x-pack/plugins/infra/server/features.ts index aa2c628a23ddd8..91f82e82b33cdf 100644 --- a/x-pack/plugins/infra/server/features.ts +++ b/x-pack/plugins/infra/server/features.ts @@ -34,7 +34,12 @@ export const METRICS_FEATURE = { read: ['index-pattern'], }, alerting: { - all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + rule: { + all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, + alert: { + all: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -50,7 +55,12 @@ export const METRICS_FEATURE = { read: ['infrastructure-ui-source', 'index-pattern'], }, alerting: { - read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + rule: { + read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, + alert: { + read: [METRIC_THRESHOLD_ALERT_TYPE_ID, METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -83,7 +93,12 @@ export const LOGS_FEATURE = { read: [], }, alerting: { - all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + rule: { + all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, + alert: { + all: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -95,7 +110,12 @@ export const LOGS_FEATURE = { catalogue: ['infralogging', 'logs'], api: ['infra'], alerting: { - read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + rule: { + read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, + alert: { + read: [LOG_DOCUMENT_COUNT_ALERT_TYPE_ID], + }, }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/convert.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/convert.test.tsx new file mode 100644 index 00000000000000..5a58a7b595c90d --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/convert.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +// Default parameter values automatically added to the `convert processor` when saved +const defaultConvertParameters = { + if: undefined, + tag: undefined, + description: undefined, + target_field: undefined, + ignore_missing: undefined, + ignore_failure: undefined, +}; + +const CONVERT_TYPE = 'convert'; + +describe('Processor: Convert', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(CONVERT_TYPE); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the processor type defined + await saveNewProcessor(); + + // Expect form error as "field" and "type" are required parameters + expect(form.getErrorsMessages()).toEqual([ + 'A field value is required.', + 'A type value is required.', + ]); + }); + + test('saves with default parameter values', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + // Add "type" value (required) + form.setSelectValue('typeSelectorField', 'ip'); + + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, CONVERT_TYPE); + expect(processors[0][CONVERT_TYPE]).toEqual({ + ...defaultConvertParameters, + field: 'field_1', + type: 'ip', + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field_1'); + // Add "type" value (required) + form.setSelectValue('typeSelectorField', 'ip'); + + // Set optional parameteres + form.setInputValue('targetField.input', 'target_field'); + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + form.toggleEuiSwitch('ignoreFailureSwitch.input'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, CONVERT_TYPE); + expect(processors[0][CONVERT_TYPE]).toEqual({ + ...defaultConvertParameters, + type: 'ip', + field: 'field_1', + target_field: 'target_field', + ignore_failure: true, + ignore_missing: true, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fingerprint.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fingerprint.test.tsx new file mode 100644 index 00000000000000..7c2ca012a04607 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/fingerprint.test.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +// Default parameter values automatically added to the registered domain processor when saved +const defaultFingerprintParameters = { + if: undefined, + tag: undefined, + method: undefined, + salt: undefined, + description: undefined, + ignore_missing: undefined, + ignore_failure: undefined, + target_field: undefined, +}; + +const FINGERPRINT_TYPE = 'fingerprint'; + +describe('Processor: Fingerprint', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(FINGERPRINT_TYPE); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the type defined + await saveNewProcessor(); + + // Expect form error as "field" is required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('saves with default parameter values', async () => { + const { + actions: { saveNewProcessor }, + find, + component, + } = testBed; + + // Add "fields" value (required) + await act(async () => { + find('fieldsValueField.input').simulate('change', [{ label: 'user' }]); + }); + component.update(); + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, FINGERPRINT_TYPE); + expect(processors[0][FINGERPRINT_TYPE]).toEqual({ + ...defaultFingerprintParameters, + fields: ['user'], + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + find, + component, + } = testBed; + + // Add "fields" value (required) + await act(async () => { + find('fieldsValueField.input').simulate('change', [{ label: 'user' }]); + }); + component.update(); + + // Set optional parameteres + form.setInputValue('targetField.input', 'target_field'); + form.setSelectValue('methodsValueField', 'SHA-256'); + form.setInputValue('saltValueField.input', 'salt'); + form.toggleEuiSwitch('ignoreMissingSwitch.input'); + form.toggleEuiSwitch('ignoreFailureSwitch.input'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, FINGERPRINT_TYPE); + expect(processors[0][FINGERPRINT_TYPE]).toEqual({ + ...defaultFingerprintParameters, + fields: ['user'], + target_field: 'target_field', + method: 'SHA-256', + salt: 'salt', + ignore_missing: true, + ignore_failure: true, + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index 193e94c7aeb9e6..9dd0d6cc72de17 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -143,6 +143,7 @@ type TestSubject = | 'messageField.input' | 'mockCodeEditor' | 'tagField.input' + | 'typeSelectorField' | 'ignoreMissingSwitch.input' | 'ignoreFailureSwitch.input' | 'ifField.textarea' @@ -153,4 +154,7 @@ type TestSubject = | 'separatorValueField.input' | 'quoteValueField.input' | 'emptyValueField.input' + | 'fieldsValueField.input' + | 'saltValueField.input' + | 'methodsValueField' | 'trimSwitch.input'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/convert.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/convert.tsx index 46aee71fdee427..af671dce295599 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/convert.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/convert.tsx @@ -58,6 +58,7 @@ export const Convert: FunctionComponent = () => { { { defaultMessage: 'Boolean' } ), }, + { + value: 'ip', + text: i18n.translate('xpack.ingestPipelines.pipelineEditor.convertForm.ipOption', { + defaultMessage: 'IP', + }), + }, { value: 'auto', text: i18n.translate( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/fingerprint.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/fingerprint.tsx new file mode 100644 index 00000000000000..5e52d560020c01 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/fingerprint.tsx @@ -0,0 +1,152 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCode } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { FieldsConfig, from, to } from './shared'; +import { TargetField } from './common_fields/target_field'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { + FIELD_TYPES, + Field, + UseField, + SelectField, + ComboBoxField, + fieldValidators, +} from '../../../../../../shared_imports'; + +const fieldsConfig: FieldsConfig = { + fields: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.fingerprint.fieldNameField', { + defaultMessage: 'Fields', + }), + helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.fingerprint.fieldNameHelpText', { + defaultMessage: 'Fields to include in the fingerprint.', + }), + validations: [ + { + validator: fieldValidators.emptyField( + i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.fingerprint.fieldNameRequiredError', + { + defaultMessage: 'A field value is required.', + } + ) + ), + }, + ], + }, + salt: { + type: FIELD_TYPES.TEXT, + serializer: from.emptyStringToUndefined, + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.fingerprint.saltFieldLabel', { + defaultMessage: 'Salt (optional)', + }), + helpText: ( + + ), + }, + method: { + type: FIELD_TYPES.SELECT, + defaultValue: 'SHA-1', + serializer: (v) => (v === 'SHA-1' || v === '' ? undefined : v), + label: i18n.translate('xpack.ingestPipelines.pipelineEditor.fingerprint.methodFieldLabel', { + defaultMessage: 'Method', + }), + helpText: ( + + ), + }, +}; + +export const Fingerprint: FunctionComponent = () => { + return ( + <> + + + {'fingerprint'}, + }} + /> + } + /> + + + + + + {'fields'}, + }} + /> + } + /> + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts index 4fb4365c477b56..5e3e5f82478bd6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/index.ts @@ -17,6 +17,7 @@ export { DotExpander } from './dot_expander'; export { Drop } from './drop'; export { Enrich } from './enrich'; export { Fail } from './fail'; +export { Fingerprint } from './fingerprint'; export { Foreach } from './foreach'; export { GeoIP } from './geoip'; export { Grok } from './grok'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index b5e42ea56bdf8b..983fb0ea67bb0f 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -23,6 +23,7 @@ import { Drop, Enrich, Fail, + Fingerprint, Foreach, GeoIP, Grok, @@ -308,6 +309,20 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { defaultMessage: 'Raises an exception that halts execution', }), }, + fingerprint: { + FieldsComponent: Fingerprint, + docLinkPath: '/fingerprint-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.fingerprint', { + defaultMessage: 'Fingerprint', + }), + typeDescription: i18n.translate('xpack.ingestPipelines.processors.description.fingerprint', { + defaultMessage: 'Computes a hash of the document’s content.', + }), + getDefaultDescription: () => + i18n.translate('xpack.ingestPipelines.processors.defaultDescription.fingerprint', { + defaultMessage: 'Computes a hash of the document’s content.', + }), + }, foreach: { FieldsComponent: Foreach, docLinkPath: '/foreach-processor.html', diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 72b8bfa38491ac..30b4e2d954d2b8 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -6,15 +6,14 @@ */ import React from 'react'; -import { Observable, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; import { App } from './app'; import { LensAppProps, LensAppServices } from './types'; import { EditorFrameInstance, EditorFrameProps } from '../types'; import { Document } from '../persistence'; -import { DOC_TYPE } from '../../common'; -import { mount } from 'enzyme'; +import { makeDefaultServices, mountWithProvider } from '../mocks'; import { I18nProvider } from '@kbn/i18n/react'; import { SavedObjectSaveModal, @@ -22,31 +21,20 @@ import { } from '../../../../../src/plugins/saved_objects/public'; import { createMemoryHistory } from 'history'; import { - DataPublicPluginStart, esFilters, FilterManager, IFieldType, IIndexPattern, - UI_SETTINGS, + IndexPattern, + Query, } from '../../../../../src/plugins/data/public'; -import { navigationPluginMock } from '../../../../../src/plugins/navigation/public/mocks'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; -import { coreMock } from 'src/core/public/mocks'; -import { - LensByValueInput, - LensSavedObjectAttributes, - LensByReferenceInput, -} from '../editor_frame_service/embeddable/embeddable'; +import { LensByValueInput } from '../editor_frame_service/embeddable/embeddable'; import { SavedObjectReference } from '../../../../../src/core/types'; -import { - mockAttributeService, - createEmbeddableStateTransferMock, -} from '../../../../../src/plugins/embeddable/public/mocks'; -import { LensAttributeService } from '../lens_attribute_service'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { EmbeddableStateTransfer } from '../../../../../src/plugins/embeddable/public'; import moment from 'moment'; +import { setState, LensAppState } from '../state_management/index'; jest.mock('../editor_frame_service/editor_frame/expression_helpers'); jest.mock('src/core/public'); jest.mock('../../../../../src/plugins/saved_objects/public', () => { @@ -61,13 +49,16 @@ jest.mock('../../../../../src/plugins/saved_objects/public', () => { }; }); -const navigationStartMock = navigationPluginMock.createStartContract(); +jest.mock('lodash', () => { + const original = jest.requireActual('lodash'); -jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { - return
; + return { + ...original, + debounce: (fn: unknown) => fn, + }; }); -const { TopNavMenu } = navigationStartMock.ui; +// const navigationStartMock = navigationPluginMock.createStartContract(); function createMockFrame(): jest.Mocked { return { @@ -77,91 +68,7 @@ function createMockFrame(): jest.Mocked { const sessionIdSubject = new Subject(); -function createMockSearchService() { - let sessionIdCounter = 1; - return { - session: { - start: jest.fn(() => `sessionId-${sessionIdCounter++}`), - clear: jest.fn(), - getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), - getSession$: jest.fn(() => sessionIdSubject.asObservable()), - }, - }; -} - -function createMockFilterManager() { - const unsubscribe = jest.fn(); - - let subscriber: () => void; - let filters: unknown = []; - - return { - getUpdates$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - setFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - setAppFilters: jest.fn((newFilters: unknown[]) => { - filters = newFilters; - if (subscriber) subscriber(); - }), - getFilters: () => filters, - getGlobalFilters: () => { - // @ts-ignore - return filters.filter(esFilters.isFilterPinned); - }, - removeAll: () => { - filters = []; - subscriber(); - }, - }; -} - -function createMockQueryString() { - return { - getQuery: jest.fn(() => ({ query: '', language: 'kuery' })), - setQuery: jest.fn(), - getDefaultQuery: jest.fn(() => ({ query: '', language: 'kuery' })), - }; -} - -function createMockTimefilter() { - const unsubscribe = jest.fn(); - - let timeFilter = { from: 'now-7d', to: 'now' }; - let subscriber: () => void; - return { - getTime: jest.fn(() => timeFilter), - setTime: jest.fn((newTimeFilter) => { - timeFilter = newTimeFilter; - if (subscriber) { - subscriber(); - } - }), - getTimeUpdate$: () => ({ - subscribe: ({ next }: { next: () => void }) => { - subscriber = next; - return unsubscribe; - }, - }), - calculateBounds: jest.fn(() => ({ - min: moment('2021-01-10T04:00:00.000Z'), - max: moment('2021-01-10T08:00:00.000Z'), - })), - getBounds: jest.fn(() => timeFilter), - getRefreshInterval: () => {}, - getRefreshIntervalDefaults: () => {}, - getAutoRefreshFetch$: () => new Observable(), - }; -} - describe('Lens App', () => { - let core: ReturnType; let defaultDoc: Document; let defaultSavedObjectId: string; @@ -171,27 +78,6 @@ describe('Lens App', () => { expectedSaveAndReturnButton: { emphasize: true, testId: 'lnsApp_saveAndReturnButton' }, }; - function makeAttributeService(): LensAttributeService { - const attributeServiceMock = mockAttributeService< - LensSavedObjectAttributes, - LensByValueInput, - LensByReferenceInput - >( - DOC_TYPE, - { - saveMethod: jest.fn(), - unwrapMethod: jest.fn(), - checkForDuplicateTitle: jest.fn(), - }, - core - ); - attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); - attributeServiceMock.wrapAttributes = jest - .fn() - .mockResolvedValue({ savedObjectId: defaultSavedObjectId }); - return attributeServiceMock; - } - function makeDefaultProps(): jest.Mocked { return { editorFrame: createMockFrame(), @@ -203,64 +89,15 @@ describe('Lens App', () => { }; } - function makeDefaultServices(): jest.Mocked { - return { - http: core.http, - chrome: core.chrome, - overlays: core.overlays, - uiSettings: core.uiSettings, - navigation: navigationStartMock, - notifications: core.notifications, - attributeService: makeAttributeService(), - savedObjectsClient: core.savedObjects.client, - dashboardFeatureFlag: { allowByValueEmbeddables: false }, - stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, - getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), - application: { - ...core.application, - capabilities: { - ...core.application.capabilities, - visualize: { save: true, saveQuery: true, show: true }, - }, - getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), - }, - data: ({ - query: { - filterManager: createMockFilterManager(), - timefilter: { - timefilter: createMockTimefilter(), - }, - queryString: createMockQueryString(), - state$: new Observable(), - }, - indexPatterns: { - get: jest.fn((id) => { - return new Promise((resolve) => resolve({ id })); - }), - }, - search: createMockSearchService(), - nowProvider: { - get: jest.fn(), - }, - } as unknown) as DataPublicPluginStart, - storage: { - get: jest.fn(), - set: jest.fn(), - remove: jest.fn(), - clear: jest.fn(), - }, - }; - } - - function mountWith({ - props: incomingProps, - services: incomingServices, + async function mountWith({ + props = makeDefaultProps(), + services = makeDefaultServices(sessionIdSubject), + storePreloadedState, }: { props?: jest.Mocked; services?: jest.Mocked; + storePreloadedState?: Partial; }) { - const props = incomingProps ?? makeDefaultProps(); - const services = incomingServices ?? makeDefaultServices(); const wrappingComponent: React.FC<{ children: React.ReactNode; }> = ({ children }) => { @@ -270,61 +107,40 @@ describe('Lens App', () => { ); }; + + const { instance, lensStore } = await mountWithProvider( + , + services.data, + storePreloadedState, + wrappingComponent + ); + const frame = props.editorFrame as ReturnType; - const component = mount(, { wrappingComponent }); - return { component, frame, props, services }; + return { instance, frame, props, services, lensStore }; } beforeEach(() => { - core = coreMock.createStart({ basePath: '/testbasepath' }); defaultSavedObjectId = '1234'; defaultDoc = ({ savedObjectId: defaultSavedObjectId, title: 'An extremely cool default document!', expression: 'definitely a valid expression', state: { - query: 'kuery', + query: 'lucene', filters: [{ query: { match_phrase: { src: 'test' } } }], }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], } as unknown) as Document; - - core.uiSettings.get.mockImplementation( - jest.fn((type) => { - if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { - return { from: 'now-7d', to: 'now' }; - } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { - return 'kuery'; - } else if (type === 'state:storeInSessionStorage') { - return false; - } else { - return []; - } - }) - ); }); - it('renders the editor frame', () => { - const { frame } = mountWith({}); + it('renders the editor frame', async () => { + const { frame } = await mountWith({}); expect(frame.EditorFrameContainer.mock.calls).toMatchInlineSnapshot(` Array [ Array [ Object { - "dateRange": Object { - "fromDate": "2021-01-10T04:00:00.000Z", - "toDate": "2021-01-10T08:00:00.000Z", - }, - "doc": undefined, - "filters": Array [], "initialContext": undefined, - "onChange": [Function], "onError": [Function], - "query": Object { - "language": "kuery", - "query": "", - }, - "savedQuery": undefined, - "searchSessionId": "sessionId-1", "showNoDataPopover": [Function], }, Object {}, @@ -333,13 +149,8 @@ describe('Lens App', () => { `); }); - it('clears app filters on load', () => { - const { services } = mountWith({}); - expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([]); - }); - - it('passes global filters to frame', async () => { - const services = makeDefaultServices(); + it('updates global filters with store state', async () => { + const services = makeDefaultServices(sessionIdSubject); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); @@ -349,25 +160,28 @@ describe('Lens App', () => { services.data.query.filterManager.getGlobalFilters = jest.fn().mockImplementation(() => { return [pinnedFilter]; }); - const { component, frame } = mountWith({ services }); + const { instance, lensStore } = await mountWith({ services }); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, - query: { query: '', language: 'kuery' }, + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + query: { query: '', language: 'lucene' }, filters: [pinnedFilter], + resolvedDateRange: { + fromDate: '2021-01-10T04:00:00.000Z', + toDate: '2021-01-10T08:00:00.000Z', + }, }), - {} - ); + }); + expect(services.data.query.filterManager.getFilters).not.toHaveBeenCalled(); }); - it('displays errors from the frame in a toast', () => { - const { component, frame, services } = mountWith({}); + it('displays errors from the frame in a toast', async () => { + const { instance, frame, services } = await mountWith({}); const onError = frame.EditorFrameContainer.mock.calls[0][0].onError; onError({ message: 'error' }); - component.update(); + instance.update(); expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); }); @@ -384,7 +198,7 @@ describe('Lens App', () => { } as unknown) as Document; it('sets breadcrumbs when the document title changes', async () => { - const { component, services } = mountWith({}); + const { instance, services, lensStore } = await mountWith({}); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ { @@ -395,9 +209,13 @@ describe('Lens App', () => { { text: 'Create' }, ]); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(breadcrumbDoc); await act(async () => { - component.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + lensStore.dispatch( + setState({ + persistedDoc: breadcrumbDoc, + }) + ); }); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -412,10 +230,17 @@ describe('Lens App', () => { it('sets originatingApp breadcrumb when the document title changes', async () => { const props = makeDefaultProps(); - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); props.incomingState = { originatingApp: 'coolContainer' }; services.getOriginatingAppName = jest.fn(() => 'The Coolest Container Ever Made'); - const { component } = mountWith({ props, services }); + + const { instance, lensStore } = await mountWith({ + props, + services, + storePreloadedState: { + isLinkedToOriginatingApp: true, + }, + }); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ { text: 'The Coolest Container Ever Made', onClick: expect.anything() }, @@ -427,9 +252,14 @@ describe('Lens App', () => { { text: 'Create' }, ]); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(breadcrumbDoc); await act(async () => { - component.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: breadcrumbDocSavedObjectId } }); + + lensStore.dispatch( + setState({ + persistedDoc: breadcrumbDoc, + }) + ); }); expect(services.chrome.setBreadcrumbs).toHaveBeenCalledWith([ @@ -445,99 +275,36 @@ describe('Lens App', () => { }); describe('persistence', () => { - it('does not load a document if there is no initial input', () => { - const { services } = mountWith({}); - expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); - }); - it('loads a document and uses query and filters if initial input is provided', async () => { - const { component, frame, services } = mountWith({}); - services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + const { instance, lensStore, services } = await mountWith({}); + const document = ({ savedObjectId: defaultSavedObjectId, state: { query: 'fake query', filters: [{ query: { match_phrase: { src: 'test' } } }], }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], - }); + } as unknown) as Document; - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + act(() => { + lensStore.dispatch( + setState({ + query: ('fake query' as unknown) as Query, + indexPatternsForTopNav: ([{ id: '1' }] as unknown) as IndexPattern[], + lastKnownDoc: document, + persistedDoc: document, + }) + ); }); + instance.update(); - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); - expect(services.data.indexPatterns.get).toHaveBeenCalledWith('1'); - expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ - { query: { match_phrase: { src: 'test' } } }, - ]); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: 'fake query', indexPatterns: [{ id: '1' }], }), {} ); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - doc: expect.objectContaining({ - savedObjectId: defaultSavedObjectId, - state: expect.objectContaining({ - query: 'fake query', - filters: [{ query: { match_phrase: { src: 'test' } } }], - }), - }), - }), - {} - ); - }); - - it('does not load documents on sequential renders unless the id changes', async () => { - const { services, component } = mountWith({}); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: '5678' } }); - }); - - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); - }); - - it('handles document load errors', async () => { - const services = makeDefaultServices(); - services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load'); - const { component, props } = mountWith({ services }); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ - savedObjectId: defaultSavedObjectId, - }); - expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); - expect(props.redirectTo).toHaveBeenCalled(); - }); - - it('adds to the recently accessed list on load', async () => { - const { component, services } = mountWith({}); - - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); - }); - expect(services.chrome.recentlyAccessed.add).toHaveBeenCalledWith( - '/app/lens#/edit/1234', - 'An extremely cool default document!', - '1234' - ); }); describe('save buttons', () => { @@ -584,7 +351,7 @@ describe('Lens App', () => { : undefined, }; - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() .mockImplementation(async ({ savedObjectId }) => ({ @@ -599,39 +366,27 @@ describe('Lens App', () => { }, } as jest.ResolvedValue); - let frame: jest.Mocked = {} as jest.Mocked; - let component: ReactWrapper = {} as ReactWrapper; - await act(async () => { - const { frame: newFrame, component: newComponent } = mountWith({ services, props }); - frame = newFrame; - component = newComponent; - }); - - if (initialSavedObjectId) { - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); - } else { - expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); - } + const { frame, instance, lensStore } = await mountWith({ services, props }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; + act(() => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: { savedObjectId: initialSavedObjectId, ...lastKnownDoc } as Document, + }) + ); + }); - act(() => - onChange({ - filterableIndexPatterns: [], - doc: { savedObjectId: initialSavedObjectId, ...lastKnownDoc } as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(false); + instance.update(); + expect(getButton(instance).disableButton).toEqual(false); await act(async () => { - testSave(component, { ...saveProps }); + testSave(instance, { ...saveProps }); }); - return { props, services, component, frame }; + return { props, services, instance, frame, lensStore }; } it('shows a disabled save button when the user does not have permissions', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -639,36 +394,36 @@ describe('Lens App', () => { visualize: { save: false, saveQuery: false, show: true }, }, }; - const { component, frame } = mountWith({ services }); - expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: 'will save this' } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(true); + const { instance, lensStore } = await mountWith({ services }); + expect(getButton(instance).disableButton).toEqual(true); + act(() => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document, + isSaveable: true, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(true); }); it('shows a save button that is enabled when the frame has provided its state and does not show save and return or save as', async () => { - const { component, frame } = mountWith({}); - expect(getButton(component).disableButton).toEqual(true); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: 'will save this' } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(false); + const { instance, lensStore, services } = await mountWith({}); + expect(getButton(instance).disableButton).toEqual(true); + act(() => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({ savedObjectId: 'will save this' } as unknown) as Document, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(false); await act(async () => { - const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + const topNavMenuConfig = instance.find(services.navigation.ui.TopNavMenu).prop('config'); expect(topNavMenuConfig).not.toContainEqual( expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) ); @@ -683,7 +438,7 @@ describe('Lens App', () => { it('Shows Save and Return and Save As buttons in create by value mode with originating app', async () => { const props = makeDefaultProps(); - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.dashboardFeatureFlag = { allowByValueEmbeddables: true }; props.incomingState = { originatingApp: 'ultraDashboard', @@ -697,10 +452,16 @@ describe('Lens App', () => { } as LensByValueInput, }; - const { component } = mountWith({ props, services }); + const { instance } = await mountWith({ + props, + services, + storePreloadedState: { + isLinkedToOriginatingApp: true, + }, + }); await act(async () => { - const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + const topNavMenuConfig = instance.find(services.navigation.ui.TopNavMenu).prop('config'); expect(topNavMenuConfig).toContainEqual( expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) ); @@ -720,10 +481,15 @@ describe('Lens App', () => { originatingApp: 'ultraDashboard', }; - const { component } = mountWith({ props }); + const { instance, services } = await mountWith({ + props, + storePreloadedState: { + isLinkedToOriginatingApp: true, + }, + }); await act(async () => { - const topNavMenuConfig = component.find(TopNavMenu).prop('config'); + const topNavMenuConfig = instance.find(services.navigation.ui.TopNavMenu).prop('config'); expect(topNavMenuConfig).toContainEqual( expect.objectContaining(navMenuItems.expectedSaveAndReturnButton) ); @@ -770,7 +536,7 @@ describe('Lens App', () => { }); it('saves the latest doc as a copy', async () => { - const { props, services, component } = await save({ + const { props, services, instance } = await save({ initialSavedObjectId: defaultSavedObjectId, newCopyOnSave: true, newTitle: 'hello there', @@ -784,7 +550,7 @@ describe('Lens App', () => { ); expect(props.redirectTo).toHaveBeenCalledWith(defaultSavedObjectId); await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); expect(services.attributeService.wrapAttributes).toHaveBeenCalledTimes(1); expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( @@ -793,7 +559,7 @@ describe('Lens App', () => { }); it('saves existing docs', async () => { - const { props, services, component } = await save({ + const { props, services, instance, lensStore } = await save({ initialSavedObjectId: defaultSavedObjectId, newCopyOnSave: false, newTitle: 'hello there', @@ -808,35 +574,51 @@ describe('Lens App', () => { ); expect(props.redirectTo).not.toHaveBeenCalled(); await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + instance.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); }); - expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); + + expect(lensStore.dispatch).toHaveBeenCalledWith({ + payload: { + lastKnownDoc: expect.objectContaining({ + savedObjectId: defaultSavedObjectId, + title: 'hello there', + }), + persistedDoc: expect.objectContaining({ + savedObjectId: defaultSavedObjectId, + title: 'hello there', + }), + isLinkedToOriginatingApp: false, + }, + type: 'app/setState', + }); + expect(services.notifications.toasts.addSuccess).toHaveBeenCalledWith( "Saved 'hello there'" ); }); it('handles save failure by showing a warning, but still allows another save', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() .mockRejectedValue({ message: 'failed' }); - const { component, props, frame } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ id: undefined } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + const { instance, props, lensStore } = await mountWith({ services }); + act(() => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({ id: undefined } as unknown) as Document, + }) + ); + }); + + instance.update(); await act(async () => { - testSave(component, { newCopyOnSave: false, newTitle: 'hello there' }); + testSave(instance, { newCopyOnSave: false, newTitle: 'hello there' }); }); expect(props.redirectTo).not.toHaveBeenCalled(); - expect(getButton(component).disableButton).toEqual(false); + expect(getButton(instance).disableButton).toEqual(false); }); it('saves new doc and redirects to originating app', async () => { @@ -895,28 +677,29 @@ describe('Lens App', () => { }); it('checks for duplicate title before saving', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.attributeService.wrapAttributes = jest .fn() .mockReturnValue(Promise.resolve({ savedObjectId: '123' })); - const { component, frame } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: '123' } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + const { instance, lensStore } = await mountWith({ services }); await act(async () => { - component.setProps({ initialInput: { savedObjectId: '123' } }); - getButton(component).run(component.getDOMNode()); + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({ savedObjectId: '123' } as unknown) as Document, + }) + ); }); - component.update(); + + instance.update(); + await act(async () => { + instance.setProps({ initialInput: { savedObjectId: '123' } }); + getButton(instance).run(instance.getDOMNode()); + }); + instance.update(); const onTitleDuplicate = jest.fn(); await act(async () => { - component.find(SavedObjectSaveModal).prop('onSave')({ + instance.find(SavedObjectSaveModal).prop('onSave')({ onTitleDuplicate, isTitleDuplicateConfirmed: false, newCopyOnSave: false, @@ -933,19 +716,20 @@ describe('Lens App', () => { }); it('does not show the copy button on first save', async () => { - const { component, frame } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - await act(async () => getButton(component).run(component.getDOMNode())); - component.update(); - expect(component.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); + const { instance, lensStore } = await mountWith({}); + await act(async () => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({} as unknown) as Document, + }) + ); + }); + + instance.update(); + await act(async () => getButton(instance).run(instance.getDOMNode())); + instance.update(); + expect(instance.find(SavedObjectSaveModal).prop('showCopyOnSave')).toEqual(false); }); }); }); @@ -960,38 +744,38 @@ describe('Lens App', () => { } it('should be disabled when no data is available', async () => { - const { component, frame } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(true); + const { instance, lensStore } = await mountWith({}); + await act(async () => { + lensStore.dispatch( + setState({ + isSaveable: true, + lastKnownDoc: ({} as unknown) as Document, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(true); }); it('should disable download when not saveable', async () => { - const { component, frame } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: false, - activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, - }) - ); + const { instance, lensStore } = await mountWith({}); - component.update(); - expect(getButton(component).disableButton).toEqual(true); + await act(async () => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({} as unknown) as Document, + isSaveable: false, + activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, + }) + ); + }); + + instance.update(); + expect(getButton(instance).disableButton).toEqual(true); }); it('should still be enabled even if the user is missing save permissions', async () => { - const services = makeDefaultServices(); + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -1000,59 +784,63 @@ describe('Lens App', () => { }, }; - const { component, frame } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - await act(async () => - onChange({ - filterableIndexPatterns: [], - doc: ({} as unknown) as Document, - isSaveable: true, - activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, - }) - ); - component.update(); - expect(getButton(component).disableButton).toEqual(false); + const { instance, lensStore } = await mountWith({ services }); + await act(async () => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({} as unknown) as Document, + isSaveable: true, + activeData: { layer1: { type: 'datatable', columns: [], rows: [] } }, + }) + ); + }); + instance.update(); + expect(getButton(instance).disableButton).toEqual(false); }); }); describe('query bar state management', () => { - it('uses the default time and query language settings', () => { - const { frame } = mountWith({}); - expect(TopNavMenu).toHaveBeenCalledWith( + it('uses the default time and query language settings', async () => { + const { lensStore, services } = await mountWith({}); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ - query: { query: '', language: 'kuery' }, + query: { query: '', language: 'lucene' }, dateRangeFrom: 'now-7d', dateRangeTo: 'now', }), {} ); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, - query: { query: '', language: 'kuery' }, + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + query: { query: '', language: 'lucene' }, + resolvedDateRange: { + fromDate: '2021-01-10T04:00:00.000Z', + toDate: '2021-01-10T08:00:00.000Z', + }, }), - {} - ); + }); }); it('updates the index patterns when the editor frame is changed', async () => { - const { component, frame } = mountWith({}); - expect(TopNavMenu).toHaveBeenCalledWith( + const { instance, lensStore, services } = await mountWith({}); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [], }), {} ); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; await act(async () => { - onChange({ - filterableIndexPatterns: ['1'], - doc: ({ id: undefined } as unknown) as Document, - isSaveable: true, - }); + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [{ id: '1' }] as IndexPattern[], + lastKnownDoc: ({} as unknown) as Document, + isSaveable: true, + }) + ); }); - component.update(); - expect(TopNavMenu).toHaveBeenCalledWith( + instance.update(); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ indexPatterns: [{ id: '1' }], }), @@ -1060,14 +848,16 @@ describe('Lens App', () => { ); // Do it again to verify that the dirty checking is done right await act(async () => { - onChange({ - filterableIndexPatterns: ['2'], - doc: ({ id: undefined } as unknown) as Document, - isSaveable: true, - }); + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [{ id: '2' }] as IndexPattern[], + lastKnownDoc: ({} as unknown) as Document, + isSaveable: true, + }) + ); }); - component.update(); - expect(TopNavMenu).toHaveBeenLastCalledWith( + instance.update(); + expect(services.navigation.ui.TopNavMenu).toHaveBeenLastCalledWith( expect.objectContaining({ indexPatterns: [{ id: '2' }], }), @@ -1075,20 +865,20 @@ describe('Lens App', () => { ); }); - it('updates the editor frame when the user changes query or time in the search bar', () => { - const { component, frame, services } = mountWith({}); + it('updates the editor frame when the user changes query or time in the search bar', async () => { + const { instance, services, lensStore } = await mountWith({}); (services.data.query.timefilter.timefilter.calculateBounds as jest.Mock).mockReturnValue({ min: moment('2021-01-09T04:00:00.000Z'), max: moment('2021-01-09T08:00:00.000Z'), }); act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - component.update(); - expect(TopNavMenu).toHaveBeenCalledWith( + instance.update(); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: 'new', language: 'lucene' }, dateRangeFrom: 'now-14d', @@ -1100,64 +890,75 @@ describe('Lens App', () => { from: 'now-14d', to: 'now-7d', }); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - dateRange: { fromDate: '2021-01-09T04:00:00.000Z', toDate: '2021-01-09T08:00:00.000Z' }, + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ query: { query: 'new', language: 'lucene' }, + resolvedDateRange: { + fromDate: '2021-01-09T04:00:00.000Z', + toDate: '2021-01-09T08:00:00.000Z', + }, }), - {} - ); + }); }); - it('updates the filters when the user changes them', () => { - const { component, frame, services } = mountWith({}); + it('updates the filters when the user changes them', async () => { + const { instance, services, lensStore } = await mountWith({}); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + filters: [], + }), + }); act(() => services.data.query.filterManager.setFilters([ esFilters.buildExistsFilter(field, indexPattern), ]) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ filters: [esFilters.buildExistsFilter(field, indexPattern)], }), - {} - ); + }); }); - it('updates the searchSessionId when the user changes query or time in the search bar', () => { - const { component, frame, services } = mountWith({}); + it('updates the searchSessionId when the user changes query or time in the search bar', async () => { + const { instance, services, lensStore } = await mountWith({}); + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-1`, + }), + }); + act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: '', language: 'lucene' }, }) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-1`, - }), - {} - ); + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-2`, + }), + }); // trigger again, this time changing just the query act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-3`, }), - {} - ); - + }); const indexPattern = ({ id: 'index1' } as unknown) as IIndexPattern; const field = ({ name: 'myfield' } as unknown) as IFieldType; act(() => @@ -1165,19 +966,18 @@ describe('Lens App', () => { esFilters.buildExistsFilter(field, indexPattern), ]) ); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-3`, + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-4`, }), - {} - ); + }); }); }); describe('saved query handling', () => { - it('does not allow saving when the user is missing the saveQuery permission', () => { - const services = makeDefaultServices(); + it('does not allow saving when the user is missing the saveQuery permission', async () => { + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -1185,16 +985,16 @@ describe('Lens App', () => { visualize: { save: false, saveQuery: false, show: true }, }, }; - mountWith({ services }); - expect(TopNavMenu).toHaveBeenCalledWith( + await mountWith({ services }); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showSaveQuery: false }), {} ); }); - it('persists the saved query ID when the query is saved', () => { - const { component } = mountWith({}); - expect(TopNavMenu).toHaveBeenCalledWith( + it('persists the saved query ID when the query is saved', async () => { + const { instance, services } = await mountWith({}); + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ showSaveQuery: true, savedQuery: undefined, @@ -1205,7 +1005,7 @@ describe('Lens App', () => { {} ); act(() => { - component.find(TopNavMenu).prop('onSaved')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1214,7 +1014,7 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ savedQuery: { id: '1', @@ -1229,10 +1029,10 @@ describe('Lens App', () => { ); }); - it('changes the saved query ID when the query is updated', () => { - const { component } = mountWith({}); + it('changes the saved query ID when the query is updated', async () => { + const { instance, services } = await mountWith({}); act(() => { - component.find(TopNavMenu).prop('onSaved')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1242,7 +1042,7 @@ describe('Lens App', () => { }); }); act(() => { - component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSavedQueryUpdated')!({ id: '2', attributes: { title: 'new title', @@ -1251,7 +1051,7 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ savedQuery: { id: '2', @@ -1266,10 +1066,10 @@ describe('Lens App', () => { ); }); - it('updates the query if saved query is selected', () => { - const { component } = mountWith({}); + it('updates the query if saved query is selected', async () => { + const { instance, services } = await mountWith({}); act(() => { - component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSavedQueryUpdated')!({ id: '2', attributes: { title: 'new title', @@ -1278,7 +1078,7 @@ describe('Lens App', () => { }, }); }); - expect(TopNavMenu).toHaveBeenCalledWith( + expect(services.navigation.ui.TopNavMenu).toHaveBeenCalledWith( expect.objectContaining({ query: { query: 'abc:def', language: 'lucene' }, }), @@ -1286,10 +1086,10 @@ describe('Lens App', () => { ); }); - it('clears all existing unpinned filters when the active saved query is cleared', () => { - const { component, frame, services } = mountWith({}); + it('clears all existing unpinned filters when the active saved query is cleared', async () => { + const { instance, services, lensStore } = await mountWith({}); act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) @@ -1301,23 +1101,22 @@ describe('Lens App', () => { const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); - component.update(); - act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenLastCalledWith( - expect.objectContaining({ + instance.update(); + act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ filters: [pinned], }), - {} - ); + }); }); }); describe('search session id management', () => { - it('updates the searchSessionId when the query is updated', () => { - const { component, frame } = mountWith({}); + it('updates the searchSessionId when the query is updated', async () => { + const { instance, lensStore, services } = await mountWith({}); act(() => { - component.find(TopNavMenu).prop('onSaved')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSaved')!({ id: '1', attributes: { title: '', @@ -1327,7 +1126,7 @@ describe('Lens App', () => { }); }); act(() => { - component.find(TopNavMenu).prop('onSavedQueryUpdated')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onSavedQueryUpdated')!({ id: '2', attributes: { title: 'new title', @@ -1336,37 +1135,18 @@ describe('Lens App', () => { }, }); }); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ searchSessionId: `sessionId-2`, }), - {} - ); - }); - - it('re-renders the frame if session id changes from the outside', async () => { - const services = makeDefaultServices(); - const { frame } = mountWith({ props: undefined, services }); - - act(() => { - sessionIdSubject.next('new-session-id'); - }); - await act(async () => { - await new Promise((r) => setTimeout(r, 0)); }); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `new-session-id`, - }), - {} - ); }); - it('updates the searchSessionId when the active saved query is cleared', () => { - const { component, frame, services } = mountWith({}); + it('updates the searchSessionId when the active saved query is cleared', async () => { + const { instance, services, lensStore } = await mountWith({}); act(() => - component.find(TopNavMenu).prop('onQuerySubmit')!({ + instance.find(services.navigation.ui.TopNavMenu).prop('onQuerySubmit')!({ dateRange: { from: 'now-14d', to: 'now-7d' }, query: { query: 'new', language: 'lucene' }, }) @@ -1378,15 +1158,14 @@ describe('Lens App', () => { const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); act(() => services.data.query.filterManager.setFilters([pinned, unpinned])); - component.update(); - act(() => component.find(TopNavMenu).prop('onClearSavedQuery')!()); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, + instance.update(); + act(() => instance.find(services.navigation.ui.TopNavMenu).prop('onClearSavedQuery')!()); + instance.update(); + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-4`, }), - {} - ); + }); }); const mockUpdate = { @@ -1407,70 +1186,39 @@ describe('Lens App', () => { activeData: undefined, }; - it('does not update the searchSessionId when the state changes', () => { - const { component, frame } = mountWith({}); - act(() => { - component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); - }); - component.update(); - expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, - }), - {} - ); - }); - - it('does update the searchSessionId when the state changes and too much time passed', () => { - const { component, frame, services } = mountWith({}); - - // time range is 100,000ms ago to 30,000ms ago (that's a lag of 30 percent) - (services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); - (services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ - from: 'now-2m', - to: 'now', - }); - (services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ - min: moment(Date.now() - 100000), - max: moment(Date.now() - 30000), - }); + it('updates the state if session id changes from the outside', async () => { + const services = makeDefaultServices(sessionIdSubject); + const { lensStore } = await mountWith({ props: undefined, services }); act(() => { - component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); + sessionIdSubject.next('new-session-id'); }); - component.update(); - expect(frame.EditorFrameContainer).toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, - }), - {} - ); - }); - - it('does not update the searchSessionId when the state changes and too little time has passed', () => { - const { component, frame, services } = mountWith({}); - - // time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update) - (services.data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300)); - (services.data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ - from: 'now-2m', - to: 'now', + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); }); - (services.data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ - min: moment(Date.now() - 100000), - max: moment(Date.now() - 300), + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `new-session-id`, + }), }); + }); + it('does not update the searchSessionId when the state changes', async () => { + const { lensStore } = await mountWith({}); act(() => { - component.find(frame.EditorFrameContainer).prop('onChange')(mockUpdate); + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [], + lastKnownDoc: mockUpdate.doc, + isSaveable: true, + }) + ); }); - component.update(); - expect(frame.EditorFrameContainer).not.toHaveBeenCalledWith( - expect.objectContaining({ - searchSessionId: `sessionId-2`, + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + searchSessionId: `sessionId-1`, }), - {} - ); + }); }); }); @@ -1483,16 +1231,16 @@ describe('Lens App', () => { confirmLeave = jest.fn(); }); - it('should not show a confirm message if there is no expression to save', () => { - const { props } = mountWith({}); + it('should not show a confirm message if there is no expression to save', async () => { + const { props } = await mountWith({}); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); - it('does not confirm if the user is missing save permissions', () => { - const services = makeDefaultServices(); + it('does not confirm if the user is missing save permissions', async () => { + const services = makeDefaultServices(sessionIdSubject); services.application = { ...services.application, capabilities: { @@ -1500,36 +1248,36 @@ describe('Lens App', () => { visualize: { save: false, saveQuery: false, show: true }, }, }; - const { component, frame, props } = mountWith({ services }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ - savedObjectId: undefined, - references: [], - } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + const { instance, props, lensStore } = await mountWith({ services }); + act(() => { + lensStore.dispatch( + setState({ + indexPatternsForTopNav: [] as IndexPattern[], + lastKnownDoc: ({ + savedObjectId: undefined, + references: [], + } as unknown) as Document, + isSaveable: true, + }) + ); + }); + instance.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); expect(confirmLeave).not.toHaveBeenCalled(); }); - it('should confirm when leaving with an unsaved doc', () => { - const { component, frame, props } = mountWith({}); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: undefined, state: {} } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); + it('should confirm when leaving with an unsaved doc', async () => { + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + lastKnownDoc: ({ savedObjectId: undefined, state: {} } as unknown) as Document, + isSaveable: true, + }) + ); + }); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); @@ -1537,22 +1285,19 @@ describe('Lens App', () => { }); it('should confirm when leaving with unsaved changes to an existing doc', async () => { - const { component, frame, props } = mountWith({}); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + persistedDoc: defaultDoc, + lastKnownDoc: ({ + savedObjectId: defaultSavedObjectId, + references: [], + } as unknown) as Document, + isSaveable: true, + }) + ); }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ - savedObjectId: defaultSavedObjectId, - references: [], - } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); @@ -1560,19 +1305,16 @@ describe('Lens App', () => { }); it('should not confirm when changes are saved', async () => { - const { component, frame, props } = mountWith({}); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + lastKnownDoc: defaultDoc, + persistedDoc: defaultDoc, + isSaveable: true, + }) + ); }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: defaultDoc, - isSaveable: true, - }) - ); - component.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(defaultLeave).toHaveBeenCalled(); @@ -1580,19 +1322,19 @@ describe('Lens App', () => { }); it('should confirm when the latest doc is invalid', async () => { - const { component, frame, props } = mountWith({}); - await act(async () => { - component.setProps({ initialInput: { savedObjectId: defaultSavedObjectId } }); + const { lensStore, props } = await mountWith({}); + act(() => { + lensStore.dispatch( + setState({ + persistedDoc: defaultDoc, + lastKnownDoc: ({ + savedObjectId: defaultSavedObjectId, + references: [], + } as unknown) as Document, + isSaveable: true, + }) + ); }); - const onChange = frame.EditorFrameContainer.mock.calls[0][0].onChange; - act(() => - onChange({ - filterableIndexPatterns: [], - doc: ({ savedObjectId: defaultSavedObjectId, references: [] } as unknown) as Document, - isSaveable: true, - }) - ); - component.update(); const lastCall = props.onAppLeave.mock.calls[props.onAppLeave.mock.calls.length - 1][0]; lastCall({ default: defaultLeave, confirm: confirmLeave }); expect(confirmLeave).toHaveBeenCalled(); diff --git a/x-pack/plugins/lens/public/app_plugin/app.tsx b/x-pack/plugins/lens/public/app_plugin/app.tsx index c172f36913c217..61ed2934a40011 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.tsx @@ -7,49 +7,38 @@ import './app.scss'; -import _ from 'lodash'; -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import { isEqual, partition } from 'lodash'; +import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { Toast } from 'kibana/public'; import { VisualizeFieldContext } from 'src/plugins/ui_actions/public'; -import { Datatable } from 'src/plugins/expressions/public'; import { EuiBreadcrumb } from '@elastic/eui'; -import { delay, finalize, switchMap, tap } from 'rxjs/operators'; -import { downloadMultipleAs } from '../../../../../src/plugins/share/public'; import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../../../../src/plugins/kibana_utils/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { - OnSaveProps, - checkForDuplicateTitle, -} from '../../../../../src/plugins/saved_objects/public'; +import { checkForDuplicateTitle } from '../../../../../src/plugins/saved_objects/public'; import { injectFilterReferences } from '../persistence'; import { trackUiEvent } from '../lens_ui_telemetry'; -import { - DataPublicPluginStart, - esFilters, - exporters, - Filter, - IndexPattern as IndexPatternInstance, - IndexPatternsContract, - Query, - SavedQuery, - syncQueryStateWithUrl, - waitUntilNextSessionCompletes$, -} from '../../../../../src/plugins/data/public'; -import { LENS_EMBEDDABLE_TYPE, getFullPath, APP_ID } from '../../common'; -import { LensAppProps, LensAppServices, LensAppState } from './types'; -import { getLensTopNavConfig } from './lens_top_nav'; +import { esFilters, syncQueryStateWithUrl } from '../../../../../src/plugins/data/public'; +import { getFullPath, APP_ID } from '../../common'; +import { LensAppProps, LensAppServices, RunSave } from './types'; +import { LensTopNavMenu } from './lens_top_nav'; import { Document } from '../persistence'; import { SaveModal } from './save_modal'; import { LensByReferenceInput, LensEmbeddableInput, } from '../editor_frame_service/embeddable/embeddable'; -import { useTimeRange } from './time_range'; import { EditorFrameInstance } from '../types'; +import { + setState as setAppState, + useLensSelector, + useLensDispatch, + LensAppState, + DispatchSetState, +} from '../state_management'; export function App({ history, @@ -67,7 +56,6 @@ export function App({ data, chrome, overlays, - navigation, uiSettings, application, stateTransfer, @@ -81,29 +69,18 @@ export function App({ dashboardFeatureFlag, } = useKibana().services; - const startSession = useCallback(() => data.search.session.start(), [data.search.session]); - - const [state, setState] = useState(() => { - return { - query: data.query.queryString.getQuery(), - // Do not use app-specific filters from previous app, - // only if Lens was opened with the intention to visualize a field (e.g. coming from Discover) - filters: !initialContext - ? data.query.filterManager.getGlobalFilters() - : data.query.filterManager.getFilters(), - isLoading: Boolean(initialInput), - indexPatternsForTopNav: [], - isLinkedToOriginatingApp: Boolean(incomingState?.originatingApp), - isSaveable: false, - searchSessionId: startSession(), - }; - }); + const dispatch = useLensDispatch(); + const dispatchSetState: DispatchSetState = useCallback( + (state: Partial) => dispatch(setAppState(state)), + [dispatch] + ); + + const appState = useLensSelector((state) => state.app); // Used to show a popover that guides the user towards changing the date range when no data is available. const [indicateNoData, setIndicateNoData] = useState(false); const [isSaveModalVisible, setIsSaveModalVisible] = useState(false); - - const { lastKnownDoc } = state; + const { lastKnownDoc } = appState; const showNoDataPopover = useCallback(() => { setIndicateNoData(true); @@ -116,19 +93,10 @@ export function App({ }, [ setIndicateNoData, indicateNoData, - state.query, - state.filters, - state.indexPatternsForTopNav, - state.searchSessionId, + appState.indexPatternsForTopNav, + appState.searchSessionId, ]); - const { resolvedDateRange, from: fromDate, to: toDate } = useTimeRange( - data, - state.lastKnownDoc, - setState, - state.searchSessionId - ); - const onError = useCallback( (e: { message: string }) => notifications.toasts.addDanger({ @@ -142,56 +110,13 @@ export function App({ Boolean( // Temporarily required until the 'by value' paradigm is default. dashboardFeatureFlag.allowByValueEmbeddables && - state.isLinkedToOriginatingApp && + appState.isLinkedToOriginatingApp && !(initialInput as LensByReferenceInput)?.savedObjectId ), - [dashboardFeatureFlag.allowByValueEmbeddables, state.isLinkedToOriginatingApp, initialInput] + [dashboardFeatureFlag.allowByValueEmbeddables, appState.isLinkedToOriginatingApp, initialInput] ); useEffect(() => { - // Clear app-specific filters when navigating to Lens. Necessary because Lens - // can be loaded without a full page refresh. If the user navigates to Lens from Discover - // we keep the filters - if (!initialContext) { - data.query.filterManager.setAppFilters([]); - } - - const filterSubscription = data.query.filterManager.getUpdates$().subscribe({ - next: () => { - setState((s) => ({ - ...s, - filters: data.query.filterManager.getFilters(), - searchSessionId: startSession(), - })); - trackUiEvent('app_filters_updated'); - }, - }); - - const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ - next: () => { - setState((s) => ({ - ...s, - searchSessionId: startSession(), - })); - }, - }); - - const autoRefreshSubscription = data.query.timefilter.timefilter - .getAutoRefreshFetch$() - .pipe( - tap(() => { - setState((s) => ({ - ...s, - searchSessionId: startSession(), - })); - }), - switchMap((done) => - // best way in lens to estimate that all panels are updated is to rely on search session service state - waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done)) - ) - ) - .subscribe(); - const kbnUrlStateStorage = createKbnUrlStateStorage({ history, useHash: uiSettings.get('state:storeInSessionStorage'), @@ -202,41 +127,10 @@ export function App({ kbnUrlStateStorage ); - const sessionSubscription = data.search.session - .getSession$() - // wait for a tick to filter/timerange subscribers the chance to update the session id in the state - .pipe(delay(0)) - // then update if it didn't get updated yet - .subscribe((newSessionId) => { - if (newSessionId) { - setState((prevState) => { - if (prevState.searchSessionId !== newSessionId) { - return { ...prevState, searchSessionId: newSessionId }; - } else { - return prevState; - } - }); - } - }); - return () => { stopSyncingQueryServiceStateWithUrl(); - filterSubscription.unsubscribe(); - timeSubscription.unsubscribe(); - autoRefreshSubscription.unsubscribe(); - sessionSubscription.unsubscribe(); }; - }, [ - data.query.filterManager, - data.query.timefilter.timefilter, - data.search.session, - notifications.toasts, - uiSettings, - data.query, - history, - initialContext, - startSession, - ]); + }, [data.search.session, notifications.toasts, uiSettings, data.query, history]); useEffect(() => { onAppLeave((actions) => { @@ -244,11 +138,11 @@ export function App({ // or when the user has configured something without saving if ( application.capabilities.visualize.save && - !_.isEqual( - state.persistedDoc?.state, + !isEqual( + appState.persistedDoc?.state, getLastKnownDocWithoutPinnedFilters(lastKnownDoc)?.state ) && - (state.isSaveable || state.persistedDoc) + (appState.isSaveable || appState.persistedDoc) ) { return actions.confirm( i18n.translate('xpack.lens.app.unsavedWorkMessage', { @@ -265,8 +159,8 @@ export function App({ }, [ onAppLeave, lastKnownDoc, - state.isSaveable, - state.persistedDoc, + appState.isSaveable, + appState.persistedDoc, application.capabilities.visualize.save, ]); @@ -274,7 +168,7 @@ export function App({ useEffect(() => { const isByValueMode = getIsByValueMode(); const breadcrumbs: EuiBreadcrumb[] = []; - if (state.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) { + if (appState.isLinkedToOriginatingApp && getOriginatingAppName() && redirectToOrigin) { breadcrumbs.push({ onClick: () => { redirectToOrigin(); @@ -297,113 +191,31 @@ export function App({ let currentDocTitle = i18n.translate('xpack.lens.breadcrumbsCreate', { defaultMessage: 'Create', }); - if (state.persistedDoc) { + if (appState.persistedDoc) { currentDocTitle = isByValueMode ? i18n.translate('xpack.lens.breadcrumbsByValue', { defaultMessage: 'Edit visualization' }) - : state.persistedDoc.title; + : appState.persistedDoc.title; } breadcrumbs.push({ text: currentDocTitle }); chrome.setBreadcrumbs(breadcrumbs); }, [ dashboardFeatureFlag.allowByValueEmbeddables, - state.isLinkedToOriginatingApp, getOriginatingAppName, - state.persistedDoc, redirectToOrigin, getIsByValueMode, - initialInput, application, chrome, - ]); - - useEffect(() => { - if ( - !initialInput || - (attributeService.inputIsRefType(initialInput) && - initialInput.savedObjectId === state.persistedDoc?.savedObjectId) - ) { - return; - } - - setState((s) => ({ ...s, isLoading: true })); - attributeService - .unwrapAttributes(initialInput) - .then((attributes) => { - if (!initialInput) { - return; - } - const doc = { - ...initialInput, - ...attributes, - type: LENS_EMBEDDABLE_TYPE, - }; - - if (attributeService.inputIsRefType(initialInput)) { - chrome.recentlyAccessed.add( - getFullPath(initialInput.savedObjectId), - attributes.title, - initialInput.savedObjectId - ); - } - const indexPatternIds = _.uniq( - doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) - ); - getAllIndexPatterns(indexPatternIds, data.indexPatterns) - .then(({ indexPatterns }) => { - // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters( - injectFilterReferences(doc.state.filters, doc.references) - ); - setState((s) => ({ - ...s, - isLoading: false, - ...(!_.isEqual(state.persistedDoc, doc) ? { persistedDoc: doc } : null), - lastKnownDoc: doc, - query: doc.state.query, - indexPatternsForTopNav: indexPatterns, - })); - }) - .catch((e) => { - setState((s) => ({ ...s, isLoading: false })); - redirectTo(); - }); - }) - .catch((e) => { - setState((s) => ({ ...s, isLoading: false })); - notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.docLoadingError', { - defaultMessage: 'Error loading saved document', - }) - ); - - redirectTo(); - }); - }, [ - notifications, - data.indexPatterns, - data.query.filterManager, initialInput, - attributeService, - redirectTo, - chrome.recentlyAccessed, - state.persistedDoc, + appState.isLinkedToOriginatingApp, + appState.persistedDoc, ]); const tagsIds = - state.persistedDoc && savedObjectsTagging - ? savedObjectsTagging.ui.getTagIdsFromReferences(state.persistedDoc.references) + appState.persistedDoc && savedObjectsTagging + ? savedObjectsTagging.ui.getTagIdsFromReferences(appState.persistedDoc.references) : []; - const runSave = async ( - saveProps: Omit & { - returnToOrigin: boolean; - dashboardId?: string | null; - onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; - newDescription?: string; - newTags?: string[]; - }, - options: { saveToLibrary: boolean } - ) => { + const runSave: RunSave = async (saveProps, options) => { if (!lastKnownDoc) { return; } @@ -502,10 +314,8 @@ export function App({ docToSave.title, newInput.savedObjectId ); - setState((s) => ({ - ...s, - isLinkedToOriginatingApp: false, - })); + + dispatchSetState({ isLinkedToOriginatingApp: false }); setIsSaveModalVisible(false); // remove editor state so the connection is still broken after reload @@ -519,12 +329,12 @@ export function App({ ...docToSave, ...newInput, }; - setState((s) => ({ - ...s, + + dispatchSetState({ + isLinkedToOriginatingApp: false, persistedDoc: newDoc, lastKnownDoc: newDoc, - isLinkedToOriginatingApp: false, - })); + }); setIsSaveModalVisible(false); } catch (e) { @@ -535,187 +345,37 @@ export function App({ } }; - const lastKnownDocRef = useRef(state.lastKnownDoc); - lastKnownDocRef.current = state.lastKnownDoc; - - const activeDataRef = useRef(state.activeData); - activeDataRef.current = state.activeData; - - const { TopNavMenu } = navigation.ui; - const savingToLibraryPermitted = Boolean( - state.isSaveable && application.capabilities.visualize.save - ); - const savingToDashboardPermitted = Boolean( - state.isSaveable && application.capabilities.dashboard?.showWriteControls + appState.isSaveable && application.capabilities.visualize.save ); - const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { - defaultMessage: 'unsaved', - }); - const topNavConfig = getLensTopNavConfig({ - showSaveAndReturn: Boolean( - state.isLinkedToOriginatingApp && - // Temporarily required until the 'by value' paradigm is default. - (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) - ), - enableExportToCSV: Boolean( - state.isSaveable && state.activeData && Object.keys(state.activeData).length - ), - isByValueMode: getIsByValueMode(), - allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, - showCancel: Boolean(state.isLinkedToOriginatingApp), - savingToLibraryPermitted, - savingToDashboardPermitted, - actions: { - exportToCSV: () => { - if (!state.activeData) { - return; - } - const datatables = Object.values(state.activeData); - const content = datatables.reduce>( - (memo, datatable, i) => { - // skip empty datatables - if (datatable) { - const postFix = datatables.length > 1 ? `-${i + 1}` : ''; - - memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = { - content: exporters.datatableToCSV(datatable, { - csvSeparator: uiSettings.get('csv:separator', ','), - quoteValues: uiSettings.get('csv:quoteValues', true), - formatFactory: data.fieldFormats.deserialize, - }), - type: exporters.CSV_MIME_TYPE, - }; - } - return memo; - }, - {} - ); - if (content) { - downloadMultipleAs(content); - } - }, - saveAndReturn: () => { - if (savingToDashboardPermitted && lastKnownDoc) { - // disabling the validation on app leave because the document has been saved. - onAppLeave((actions) => { - return actions.default(); - }); - runSave( - { - newTitle: lastKnownDoc.title, - newCopyOnSave: false, - isTitleDuplicateConfirmed: false, - returnToOrigin: true, - }, - { - saveToLibrary: - (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, - } - ); - } - }, - showSaveModal: () => { - if (savingToDashboardPermitted || savingToLibraryPermitted) { - setIsSaveModalVisible(true); - } - }, - cancel: () => { - if (redirectToOrigin) { - redirectToOrigin(); - } - }, - }, - }); - return ( <>
- { - const { dateRange, query } = payload; - const currentRange = data.query.timefilter.timefilter.getTime(); - if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) { - data.query.timefilter.timefilter.setTime(dateRange); - trackUiEvent('app_date_change'); - } else { - // Query has changed, renew the session id. - // Time change will be picked up by the time subscription - setState((s) => ({ - ...s, - searchSessionId: startSession(), - })); - trackUiEvent('app_query_change'); - } - setState((s) => ({ - ...s, - query: query || s.query, - })); - }} - onSaved={(savedQuery) => { - setState((s) => ({ ...s, savedQuery })); - }} - onSavedQueryUpdated={(savedQuery) => { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = data.query.filterManager.getGlobalFilters(); - data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); - setState((s) => ({ - ...s, - savedQuery: { ...savedQuery }, // Shallow query for reference issues - query: savedQuery.attributes.query, - })); - }} - onClearSavedQuery={() => { - data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); - setState((s) => ({ - ...s, - savedQuery: undefined, - filters: data.query.filterManager.getGlobalFilters(), - query: data.query.queryString.getDefaultQuery(), - })); - }} - query={state.query} - dateRangeFrom={fromDate} - dateRangeTo={toDate} + - {(!state.isLoading || state.persistedDoc) && ( + {(!appState.isAppLoading || appState.persistedDoc) && ( )}
Toast; showNoDataPopover: () => void; initialContext: VisualizeFieldContext | undefined; - setState: React.Dispatch>; - data: DataPublicPluginStart; - lastKnownDoc: React.MutableRefObject; - activeData: React.MutableRefObject | undefined>; }) { const { EditorFrameContainer } = editorFrame; return ( { - if (isSaveable !== oldIsSaveable) { - setState((s) => ({ ...s, isSaveable })); - } - if (!_.isEqual(persistedDoc, doc) && !_.isEqual(lastKnownDoc.current, doc)) { - setState((s) => ({ ...s, lastKnownDoc: doc })); - } - if (!_.isEqual(activeDataRef.current, activeData)) { - setState((s) => ({ ...s, activeData })); - } - - // Update the cached index patterns if the user made a change to any of them - if ( - indexPatternsForTopNav.length !== filterableIndexPatterns.length || - filterableIndexPatterns.some( - (id) => !indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) - ) - ) { - getAllIndexPatterns(filterableIndexPatterns, data.indexPatterns).then( - ({ indexPatterns }) => { - if (indexPatterns) { - setState((s) => ({ ...s, indexPatternsForTopNav: indexPatterns })); - } - } - ); - } - }} /> ); }); -export async function getAllIndexPatterns( - ids: string[], - indexPatternsService: IndexPatternsContract -): Promise<{ indexPatterns: IndexPatternInstance[]; rejectedIds: string[] }> { - const responses = await Promise.allSettled(ids.map((id) => indexPatternsService.get(id))); - const fullfilled = responses.filter( - (response): response is PromiseFulfilledResult => - response.status === 'fulfilled' - ); - const rejectedIds = responses - .map((_response, i) => ids[i]) - .filter((id, i) => responses[i].status === 'rejected'); - // return also the rejected ids in case we want to show something later on - return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds }; -} - function getLastKnownDocWithoutPinnedFilters(doc?: Document) { if (!doc) return undefined; - const [pinnedFilters, appFilters] = _.partition( + const [pinnedFilters, appFilters] = partition( injectFilterReferences(doc.state?.filters || [], doc.references), esFilters.isFilterPinned ); diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index f90a21b2818d47..245e964bbd2e67 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -5,11 +5,25 @@ * 2.0. */ +import { isEqual } from 'lodash'; import { i18n } from '@kbn/i18n'; +import React from 'react'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; -import { LensTopNavActions } from './types'; +import { LensAppServices, LensTopNavActions, LensTopNavMenuProps } from './types'; +import { downloadMultipleAs } from '../../../../../src/plugins/share/public'; +import { trackUiEvent } from '../lens_ui_telemetry'; +import { exporters } from '../../../../../src/plugins/data/public'; -export function getLensTopNavConfig(options: { +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { + setState as setAppState, + useLensSelector, + useLensDispatch, + LensAppState, + DispatchSetState, +} from '../state_management'; + +function getLensTopNavConfig(options: { showSaveAndReturn: boolean; enableExportToCSV: boolean; showCancel: boolean; @@ -101,6 +115,185 @@ export function getLensTopNavConfig(options: { }), }); } - return topNavMenu; } + +export const LensTopNavMenu = ({ + setHeaderActionMenu, + initialInput, + indicateNoData, + setIsSaveModalVisible, + getIsByValueMode, + runSave, + onAppLeave, + redirectToOrigin, +}: LensTopNavMenuProps) => { + const { + data, + navigation, + uiSettings, + application, + attributeService, + dashboardFeatureFlag, + } = useKibana().services; + + const dispatch = useLensDispatch(); + const dispatchSetState: DispatchSetState = React.useCallback( + (state: Partial) => dispatch(setAppState(state)), + [dispatch] + ); + + const { + isSaveable, + isLinkedToOriginatingApp, + indexPatternsForTopNav, + query, + lastKnownDoc, + activeData, + savedQuery, + } = useLensSelector((state) => state.app); + + const { TopNavMenu } = navigation.ui; + const { from, to } = data.query.timefilter.timefilter.getTime(); + + const savingToLibraryPermitted = Boolean(isSaveable && application.capabilities.visualize.save); + const savingToDashboardPermitted = Boolean( + isSaveable && application.capabilities.dashboard?.showWriteControls + ); + + const unsavedTitle = i18n.translate('xpack.lens.app.unsavedFilename', { + defaultMessage: 'unsaved', + }); + const topNavConfig = getLensTopNavConfig({ + showSaveAndReturn: Boolean( + isLinkedToOriginatingApp && + // Temporarily required until the 'by value' paradigm is default. + (dashboardFeatureFlag.allowByValueEmbeddables || Boolean(initialInput)) + ), + enableExportToCSV: Boolean(isSaveable && activeData && Object.keys(activeData).length), + isByValueMode: getIsByValueMode(), + allowByValue: dashboardFeatureFlag.allowByValueEmbeddables, + showCancel: Boolean(isLinkedToOriginatingApp), + savingToLibraryPermitted, + savingToDashboardPermitted, + actions: { + exportToCSV: () => { + if (!activeData) { + return; + } + const datatables = Object.values(activeData); + const content = datatables.reduce>( + (memo, datatable, i) => { + // skip empty datatables + if (datatable) { + const postFix = datatables.length > 1 ? `-${i + 1}` : ''; + + memo[`${lastKnownDoc?.title || unsavedTitle}${postFix}.csv`] = { + content: exporters.datatableToCSV(datatable, { + csvSeparator: uiSettings.get('csv:separator', ','), + quoteValues: uiSettings.get('csv:quoteValues', true), + formatFactory: data.fieldFormats.deserialize, + }), + type: exporters.CSV_MIME_TYPE, + }; + } + return memo; + }, + {} + ); + if (content) { + downloadMultipleAs(content); + } + }, + saveAndReturn: () => { + if (savingToDashboardPermitted && lastKnownDoc) { + // disabling the validation on app leave because the document has been saved. + onAppLeave((actions) => { + return actions.default(); + }); + runSave( + { + newTitle: lastKnownDoc.title, + newCopyOnSave: false, + isTitleDuplicateConfirmed: false, + returnToOrigin: true, + }, + { + saveToLibrary: + (initialInput && attributeService.inputIsRefType(initialInput)) ?? false, + } + ); + } + }, + showSaveModal: () => { + if (savingToDashboardPermitted || savingToLibraryPermitted) { + setIsSaveModalVisible(true); + } + }, + cancel: () => { + if (redirectToOrigin) { + redirectToOrigin(); + } + }, + }, + }); + + return ( + { + const { dateRange, query: newQuery } = payload; + const currentRange = data.query.timefilter.timefilter.getTime(); + if (dateRange.from !== currentRange.from || dateRange.to !== currentRange.to) { + data.query.timefilter.timefilter.setTime(dateRange); + trackUiEvent('app_date_change'); + } else { + // Query has changed, renew the session id. + // Time change will be picked up by the time subscription + dispatchSetState({ searchSessionId: data.search.session.start() }); + trackUiEvent('app_query_change'); + } + if (newQuery) { + if (!isEqual(newQuery, query)) { + dispatchSetState({ query: newQuery }); + } + } + }} + onSaved={(newSavedQuery) => { + dispatchSetState({ savedQuery: newSavedQuery }); + }} + onSavedQueryUpdated={(newSavedQuery) => { + const savedQueryFilters = newSavedQuery.attributes.filters || []; + const globalFilters = data.query.filterManager.getGlobalFilters(); + data.query.filterManager.setFilters([...globalFilters, ...savedQueryFilters]); + dispatchSetState({ + query: newSavedQuery.attributes.query, + savedQuery: { ...newSavedQuery }, + }); // Shallow query for reference issues + }} + onClearSavedQuery={() => { + data.query.filterManager.setFilters(data.query.filterManager.getGlobalFilters()); + dispatchSetState({ + filters: data.query.filterManager.getGlobalFilters(), + query: data.query.queryString.getDefaultQuery(), + savedQuery: undefined, + }); + }} + indexPatterns={indexPatternsForTopNav} + query={query} + dateRangeFrom={from} + dateRangeTo={to} + indicateNoData={indicateNoData} + showSearchBar={true} + showDatePicker={true} + showQueryBar={true} + showFilterBar={true} + data-test-subj="lnsApp_topNav" + screenTitle={'lens'} + appName={'lens'} + /> + ); +}; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx new file mode 100644 index 00000000000000..f2640c5c32acf9 --- /dev/null +++ b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { makeDefaultServices, mockLensStore } from '../mocks'; +import { act } from 'react-dom/test-utils'; +import { loadDocument } from './mounter'; +import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddable'; + +const defaultSavedObjectId = '1234'; + +describe('Mounter', () => { + describe('loadDocument', () => { + it('does not load a document if there is no initial input', async () => { + const services = makeDefaultServices(); + const redirectCallback = jest.fn(); + const lensStore = mockLensStore({ data: services.data }); + await loadDocument(redirectCallback, undefined, services, lensStore); + expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); + }); + + it('loads a document and uses query and filters if initial input is provided', async () => { + const services = makeDefaultServices(); + const redirectCallback = jest.fn(); + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: defaultSavedObjectId, + state: { + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], + }); + + const lensStore = await mockLensStore({ data: services.data }); + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore + ); + }); + + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + + expect(services.data.indexPatterns.get).toHaveBeenCalledWith('1'); + + expect(services.data.query.filterManager.setAppFilters).toHaveBeenCalledWith([ + { query: { match_phrase: { src: 'test' } } }, + ]); + + expect(lensStore.getState()).toEqual({ + app: expect.objectContaining({ + persistedDoc: expect.objectContaining({ + savedObjectId: defaultSavedObjectId, + state: expect.objectContaining({ + query: 'fake query', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }), + }), + }), + }); + }); + + it('does not load documents on sequential renders unless the id changes', async () => { + const redirectCallback = jest.fn(); + const services = makeDefaultServices(); + const lensStore = mockLensStore({ data: services.data }); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore + ); + }); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore + ); + }); + + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(1); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: '5678' } as LensEmbeddableInput, + services, + lensStore + ); + }); + + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledTimes(2); + }); + + it('handles document load errors', async () => { + const services = makeDefaultServices(); + const redirectCallback = jest.fn(); + + const lensStore = mockLensStore({ data: services.data }); + + services.attributeService.unwrapAttributes = jest.fn().mockRejectedValue('failed to load'); + + await act(async () => { + await loadDocument( + redirectCallback, + { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, + services, + lensStore + ); + }); + expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ + savedObjectId: defaultSavedObjectId, + }); + expect(services.notifications.toasts.addDanger).toHaveBeenCalled(); + expect(redirectCallback).toHaveBeenCalled(); + }); + + it('adds to the recently accessed list on load', async () => { + const redirectCallback = jest.fn(); + + const services = makeDefaultServices(); + const lensStore = mockLensStore({ data: services.data }); + await act(async () => { + await loadDocument( + redirectCallback, + ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput, + services, + lensStore + ); + }); + + expect(services.chrome.recentlyAccessed.add).toHaveBeenCalledWith( + '/app/lens#/edit/1234', + 'An extremely cool default document!', + '1234' + ); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index e6eb115562d378..708573e843fcf4 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -15,6 +15,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { i18n } from '@kbn/i18n'; import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public'; +import { Provider } from 'react-redux'; +import { uniq, isEqual } from 'lodash'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry'; @@ -23,7 +25,7 @@ import { App } from './app'; import { EditorFrameStart } from '../types'; import { addHelpMenuToAppChrome } from '../help_menu_util'; import { LensPluginStartDependencies } from '../plugin'; -import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID } from '../../common'; +import { LENS_EMBEDDABLE_TYPE, LENS_EDIT_BY_VALUE, APP_ID, getFullPath } from '../../common'; import { LensEmbeddableInput, LensByReferenceInput, @@ -34,6 +36,16 @@ import { LensAttributeService } from '../lens_attribute_service'; import { LensAppServices, RedirectToOriginProps, HistoryLocationState } from './types'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + makeConfigureStore, + navigateAway, + getPreloadedState, + LensRootStore, + setState, +} from '../state_management'; +import { getAllIndexPatterns, getResolvedDateRange } from '../utils'; +import { injectFilterReferences } from '../persistence'; + export async function mountApp( core: CoreSetup, params: AppMountParameters, @@ -149,8 +161,32 @@ export async function mountApp( coreStart.application.navigateToApp(embeddableEditorIncomingState?.originatingApp); } }; + const initialContext = + historyLocationState && historyLocationState.type === ACTION_VISUALIZE_LENS_FIELD + ? historyLocationState.payload + : undefined; + + // Clear app-specific filters when navigating to Lens. Necessary because Lens + // can be loaded without a full page refresh. If the user navigates to Lens from Discover + // we keep the filters + if (!initialContext) { + data.query.filterManager.setAppFilters([]); + } + + const preloadedState = getPreloadedState({ + query: data.query.queryString.getQuery(), + // Do not use app-specific filters from previous app, + // only if Lens was opened with the intention to visualize a field (e.g. coming from Discover) + filters: !initialContext + ? data.query.filterManager.getGlobalFilters() + : data.query.filterManager.getFilters(), + searchSessionId: data.search.session.start(), + resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), + isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp), + }); + + const lensStore: LensRootStore = makeConfigureStore(preloadedState, { data }); - // const featureFlagConfig = await getByValueFeatureFlag(); const EditorRenderer = React.memo( (props: { id?: string; history: History; editByValue?: boolean }) => { const redirectCallback = useCallback( @@ -160,23 +196,23 @@ export async function mountApp( [props.history] ); trackUiEvent('loaded'); + const initialInput = getInitialInput(props.id, props.editByValue); + loadDocument(redirectCallback, initialInput, lensServices, lensStore); return ( - + + + ); } ); @@ -232,5 +268,86 @@ export async function mountApp( data.search.session.clear(); unmountComponentAtNode(params.element); unlistenParentHistory(); + lensStore.dispatch(navigateAway()); }; } + +export function loadDocument( + redirectCallback: (savedObjectId?: string) => void, + initialInput: LensEmbeddableInput | undefined, + lensServices: LensAppServices, + lensStore: LensRootStore +) { + const { attributeService, chrome, notifications, data } = lensServices; + const { persistedDoc } = lensStore.getState().app; + if ( + !initialInput || + (attributeService.inputIsRefType(initialInput) && + initialInput.savedObjectId === persistedDoc?.savedObjectId) + ) { + return; + } + lensStore.dispatch(setState({ isAppLoading: true })); + + attributeService + .unwrapAttributes(initialInput) + .then((attributes) => { + if (!initialInput) { + return; + } + const doc = { + ...initialInput, + ...attributes, + type: LENS_EMBEDDABLE_TYPE, + }; + + if (attributeService.inputIsRefType(initialInput)) { + chrome.recentlyAccessed.add( + getFullPath(initialInput.savedObjectId), + attributes.title, + initialInput.savedObjectId + ); + } + const indexPatternIds = uniq( + doc.references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) + ); + getAllIndexPatterns(indexPatternIds, data.indexPatterns) + .then(({ indexPatterns }) => { + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters( + injectFilterReferences(doc.state.filters, doc.references) + ); + lensStore.dispatch( + setState({ + query: doc.state.query, + isAppLoading: false, + indexPatternsForTopNav: indexPatterns, + lastKnownDoc: doc, + ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null), + }) + ); + }) + .catch((e) => { + lensStore.dispatch( + setState({ + isAppLoading: false, + }) + ); + redirectCallback(); + }); + }) + .catch((e) => { + lensStore.dispatch( + setState({ + isAppLoading: false, + }) + ); + notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docLoadingError', { + defaultMessage: 'Error loading saved document', + }) + ); + + redirectCallback(); + }); +} diff --git a/x-pack/plugins/lens/public/app_plugin/time_range.ts b/x-pack/plugins/lens/public/app_plugin/time_range.ts deleted file mode 100644 index c9e507f3e6f132..00000000000000 --- a/x-pack/plugins/lens/public/app_plugin/time_range.ts +++ /dev/null @@ -1,84 +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 './app.scss'; - -import _ from 'lodash'; -import moment from 'moment'; -import { useEffect, useMemo } from 'react'; -import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; -import { LensAppState } from './types'; -import { Document } from '../persistence'; - -function containsDynamicMath(dateMathString: string) { - return dateMathString.includes('now'); -} - -const TIME_LAG_PERCENTAGE_LIMIT = 0.02; - -/** - * Fetches the current global time range from data plugin and restarts session - * if the fixed "now" parameter is diverging too much from the actual current time. - * @param data data plugin contract to manage current now value, time range and session - * @param lastKnownDoc Current state of the editor - * @param setState state setter for Lens app state - * @param searchSessionId current session id - */ -export function useTimeRange( - data: DataPublicPluginStart, - lastKnownDoc: Document | undefined, - setState: React.Dispatch>, - searchSessionId: string -) { - const timefilter = data.query.timefilter.timefilter; - const { from, to } = data.query.timefilter.timefilter.getTime(); - - // Need a stable reference for the frame component of the dateRange - const resolvedDateRange = useMemo(() => { - const { min, max } = timefilter.calculateBounds({ - from, - to, - }); - return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to }; - // recalculate current date range if the session gets updated because it - // might change "now" and calculateBounds depends on it internally - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [timefilter, searchSessionId, from, to]); - - useEffect(() => { - const unresolvedTimeRange = timefilter.getTime(); - if ( - !containsDynamicMath(unresolvedTimeRange.from) && - !containsDynamicMath(unresolvedTimeRange.to) - ) { - return; - } - - const { min, max } = timefilter.getBounds(); - - if (!min || !max) { - // bounds not fully specified, bailing out - return; - } - - // calculate length of currently configured range in ms - const timeRangeLength = moment.duration(max.diff(min)).asMilliseconds(); - - // calculate lag of managed "now" for date math - const nowDiff = Date.now() - data.nowProvider.get().valueOf(); - - // if the lag is signifcant, start a new session to clear the cache - if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) { - setState((s) => ({ - ...s, - searchSessionId: data.search.session.start(), - })); - } - }, [data.nowProvider, data.search.session, timefilter, lastKnownDoc, setState]); - - return { resolvedDateRange, from, to }; -} diff --git a/x-pack/plugins/lens/public/app_plugin/types.ts b/x-pack/plugins/lens/public/app_plugin/types.ts index c9143542e67bfb..72850552723f33 100644 --- a/x-pack/plugins/lens/public/app_plugin/types.ts +++ b/x-pack/plugins/lens/public/app_plugin/types.ts @@ -6,6 +6,7 @@ */ import { History } from 'history'; +import { OnSaveProps } from 'src/plugins/saved_objects/public'; import { ApplicationStart, AppMountParameters, @@ -16,14 +17,7 @@ import { OverlayStart, SavedObjectsStart, } from '../../../../../src/core/public'; -import { - DataPublicPluginStart, - Filter, - IndexPattern, - Query, - SavedQuery, -} from '../../../../../src/plugins/data/public'; -import { Document } from '../persistence'; +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddable'; import { NavigationPublicPluginStart } from '../../../../../src/plugins/navigation/public'; import { LensAttributeService } from '../lens_attribute_service'; @@ -38,28 +32,7 @@ import { EmbeddableEditorState, EmbeddableStateTransfer, } from '../../../../../src/plugins/embeddable/public'; -import { TableInspectorAdapter } from '../editor_frame_service/types'; import { EditorFrameInstance } from '../types'; - -export interface LensAppState { - isLoading: boolean; - persistedDoc?: Document; - lastKnownDoc?: Document; - - // index patterns used to determine which filters are available in the top nav. - indexPatternsForTopNav: IndexPattern[]; - - // Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb. - isLinkedToOriginatingApp?: boolean; - - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - isSaveable: boolean; - activeData?: TableInspectorAdapter; - searchSessionId: string; -} - export interface RedirectToOriginProps { input?: LensEmbeddableInput; isCopied?: boolean; @@ -82,6 +55,32 @@ export interface LensAppProps { initialContext?: VisualizeFieldContext; } +export type RunSave = ( + saveProps: Omit & { + returnToOrigin: boolean; + dashboardId?: string | null; + onTitleDuplicate?: OnSaveProps['onTitleDuplicate']; + newDescription?: string; + newTags?: string[]; + }, + options: { + saveToLibrary: boolean; + } +) => Promise; + +export interface LensTopNavMenuProps { + onAppLeave: AppMountParameters['onAppLeave']; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + + redirectToOrigin?: (props?: RedirectToOriginProps) => void; + // The initial input passed in by the container when editing. Can be either by reference or by value. + initialInput?: LensEmbeddableInput; + getIsByValueMode: () => boolean; + indicateNoData: boolean; + setIsSaveModalVisible: React.Dispatch>; + runSave: RunSave; +} + export interface HistoryLocationState { type: typeof ACTION_VISUALIZE_LENS_FIELD; payload: VisualizeFieldContext; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index f23e4c74e1a8be..351b4009240ebb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -7,6 +7,7 @@ import React, { ReactElement } from 'react'; import { ReactWrapper } from 'enzyme'; +import { setState, LensRootStore } from '../../state_management/index'; // Tests are executed in a jsdom environment who does not have sizing methods, // thus the AutoSizer will always compute a 0x0 size space @@ -28,8 +29,7 @@ jest.mock('react-virtualized-auto-sizer', () => { }); import { EuiPanel, EuiToolTip } from '@elastic/eui'; -import { mountWithIntl as mount } from '@kbn/test/jest'; -import { EditorFrame } from './editor_frame'; +import { EditorFrame, EditorFrameProps } from './editor_frame'; import { DatasourcePublicAPI, DatasourceSuggestion, Visualization } from '../../types'; import { act } from 'react-dom/test-utils'; import { coreMock } from 'src/core/public/mocks'; @@ -44,9 +44,9 @@ import { ReactExpressionRendererType } from 'src/plugins/expressions/public'; import { DragDrop } from '../../drag_drop'; import { FrameLayout } from './frame_layout'; import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks'; -import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks'; +import { mockDataPlugin, mountWithProvider } from '../../mocks'; function generateSuggestion(state = {}): DatasourceSuggestion { return { @@ -62,7 +62,7 @@ function generateSuggestion(state = {}): DatasourceSuggestion { } function getDefaultProps() { - return { + const defaultProps = { store: { save: jest.fn(), load: jest.fn(), @@ -72,18 +72,17 @@ function getDefaultProps() { onChange: jest.fn(), dateRange: { fromDate: '', toDate: '' }, query: { query: '', language: 'lucene' }, - filters: [], core: coreMock.createStart(), plugins: { uiActions: uiActionsPluginMock.createStartContract(), - data: dataPluginMock.createStartContract(), + data: mockDataPlugin(), expressions: expressionsPluginMock.createStartContract(), charts: chartPluginMock.createStartContract(), }, palettes: chartPluginMock.createPaletteRegistry(), showNoDataPopover: jest.fn(), - searchSessionId: 'sessionId', }; + return defaultProps; } describe('editor_frame', () => { @@ -133,85 +132,57 @@ describe('editor_frame', () => { describe('initialization', () => { it('should initialize initial datasource', async () => { mockVisualization.getLayerIds.mockReturnValue([]); - await act(async () => { - mount( - - ); - }); - - expect(mockDatasource.initialize).toHaveBeenCalled(); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, - it('should not initialize datasource and visualization if no initial one is specificed', () => { - act(() => { - mount( - - ); - }); + ExpressionRenderer: expressionRendererMock, + }; - expect(mockVisualization.initialize).not.toHaveBeenCalled(); - expect(mockDatasource.initialize).not.toHaveBeenCalled(); + await mountWithProvider(, props.plugins.data); + expect(mockDatasource.initialize).toHaveBeenCalled(); }); it('should initialize all datasources with state from doc', async () => { const mockDatasource3 = createMockDatasource('testDatasource3'); const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + testDatasource3: mockDatasource3, + }, - await act(async () => { - mount( - - ); + ExpressionRenderer: expressionRendererMock, + }; + + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: datasource1State, + testDatasource2: datasource2State, + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); + expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State, [], undefined, { isFullEditor: true, }); @@ -222,42 +193,40 @@ describe('editor_frame', () => { }); it('should not render something before all datasources are initialized', async () => { + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await act(async () => { - mount( - - ); + mountWithProvider(, props.plugins.data); expect(mockDatasource.renderDataPanel).not.toHaveBeenCalled(); }); expect(mockDatasource.renderDataPanel).toHaveBeenCalled(); }); it('should not initialize visualization before datasource is initialized', async () => { + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await act(async () => { - mount( - - ); + mountWithProvider(, props.plugins.data); expect(mockVisualization.initialize).not.toHaveBeenCalled(); }); @@ -265,23 +234,19 @@ describe('editor_frame', () => { }); it('should pass the public frame api into visualization initialize', async () => { - const defaultProps = getDefaultProps(); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; await act(async () => { - mount( - - ); + mountWithProvider(, props.plugins.data); expect(mockVisualization.initialize).not.toHaveBeenCalled(); }); @@ -291,33 +256,43 @@ describe('editor_frame', () => { removeLayers: expect.any(Function), query: { query: '', language: 'lucene' }, filters: [], - dateRange: { fromDate: 'now-7d', toDate: 'now' }, - availablePalettes: defaultProps.palettes, - searchSessionId: 'sessionId', + dateRange: { fromDate: '2021-01-10T04:00:00.000Z', toDate: '2021-01-10T08:00:00.000Z' }, + availablePalettes: props.palettes, + searchSessionId: 'sessionId-1', }); }); it('should add new layer on active datasource on frame api call', async () => { const initialState = { datasource2: '' }; mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState)); - await act(async () => { - mount( - , props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { testDatasource2: mockDatasource2, - }} - initialDatasourceId="testDatasource2" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); - act(() => { mockVisualization.initialize.mock.calls[0][0].addNewLayer(); }); @@ -332,22 +307,33 @@ describe('editor_frame', () => { mockDatasource2.getLayers.mockReturnValue(['abc', 'def']); mockDatasource2.removeLayer.mockReturnValue({ removed: true }); mockVisualization.getLayerIds.mockReturnValue(['first', 'abc', 'def']); - await act(async () => { - mount( - , props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { testDatasource2: mockDatasource2, - }} - initialDatasourceId="testDatasource2" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); act(() => { @@ -362,28 +348,26 @@ describe('editor_frame', () => { const initialState = {}; let databaseInitialized: ({}) => void; - await act(async () => { - mount( - - new Promise((resolve) => { - databaseInitialized = resolve; - }), - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + initialize: () => + new Promise((resolve) => { + databaseInitialized = resolve; + }), + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + + await mountWithProvider(, props.plugins.data); + await act(async () => { databaseInitialized!(initialState); }); @@ -397,25 +381,22 @@ describe('editor_frame', () => { const initialState = {}; mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - mount( - initialState }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - initialize: () => Promise.resolve(), - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { ...mockVisualization, initialize: () => initialState }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + initialize: () => Promise.resolve(), + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + + await mountWithProvider(, props.plugins.data); expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: initialState }) @@ -427,25 +408,21 @@ describe('editor_frame', () => { it('should render the resulting expression using the expression renderer', async () => { mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - instance = mount( - 'vis' }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - toExpression: () => 'datasource', - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => 'vis' }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + toExpression: () => 'datasource', + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; instance.update(); @@ -466,37 +443,34 @@ describe('editor_frame', () => { ); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); - await act(async () => { - instance = mount( - 'vis' }, - }} - datasourceMap={{ - testDatasource: mockDatasource, - testDatasource2: mockDatasource2, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - doc={{ - visualizationType: 'testVis', - title: '', - state: { - datasourceStates: { - testDatasource: {}, - testDatasource2: {}, - }, - visualization: {}, - query: { query: '', language: 'lucene' }, - filters: [], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { ...mockVisualization, toExpression: () => 'vis' }, + }, + datasourceMap: { testDatasource: mockDatasource, testDatasource2: mockDatasource2 }, + + ExpressionRenderer: expressionRendererMock, + }; + + instance = ( + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: {}, + testDatasource2: {}, }, - references: [], - }} - /> - ); - }); + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, + }) + ).instance; instance.update(); @@ -577,23 +551,18 @@ describe('editor_frame', () => { describe('state update', () => { it('should re-render config panel after state update', async () => { mockDatasource.getLayers.mockReturnValue(['first']); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, - await act(async () => { - mount( - - ); - }); + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); const updatedState = {}; const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] .setState; @@ -601,8 +570,9 @@ describe('editor_frame', () => { setDatasourceState(updatedState); }); + // TODO: temporary regression // validation requires to calls this getConfiguration API - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(7); + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(9); expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ state: updatedState, @@ -613,22 +583,18 @@ describe('editor_frame', () => { it('should re-render data panel after state update', async () => { mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); const setDatasourceState = (mockDatasource.renderDataPanel as jest.Mock).mock.calls[0][1] .setState; @@ -653,23 +619,18 @@ describe('editor_frame', () => { it('should re-render config panel with updated datasource api after datasource state update', async () => { mockDatasource.getLayers.mockReturnValue(['first']); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, - await act(async () => { - mount( - - ); - }); + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); const updatedPublicAPI: DatasourcePublicAPI = { datasourceId: 'testDatasource', @@ -684,8 +645,9 @@ describe('editor_frame', () => { setDatasourceState({}); }); + // TODO: temporary regression, selectors will help // validation requires to calls this getConfiguration API - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(7); + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(9); expect(mockVisualization.getConfiguration).toHaveBeenLastCalledWith( expect.objectContaining({ frame: expect.objectContaining({ @@ -703,37 +665,33 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); mockVisualization.getLayerIds.mockReturnValue(['first', 'second', 'third']); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, - await act(async () => { - mount( - - ); + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: {}, + testDatasource2: {}, + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); expect(mockVisualization.getConfiguration).toHaveBeenCalled(); @@ -756,36 +714,33 @@ describe('editor_frame', () => { const datasource1State = { datasource1: '' }; const datasource2State = { datasource2: '' }; - await act(async () => { - mount( - - ); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data, { + persistedDoc: { + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: datasource1State, + testDatasource2: datasource2State, + }, + visualization: {}, + query: { query: '', language: 'lucene' }, + filters: [], + }, + references: [], + }, }); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( @@ -813,22 +768,18 @@ describe('editor_frame', () => { mockDatasource.initialize.mockResolvedValue(datasourceState); mockDatasource.getLayers.mockReturnValue(['first']); - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith({ state: datasourceState, @@ -870,24 +821,20 @@ describe('editor_frame', () => { }, ]); - await act(async () => { - instance = mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // necessary to flush elements to dom synchronously instance.update(); @@ -984,49 +931,41 @@ describe('editor_frame', () => { describe('suggestions', () => { it('should fetch suggestions of currently active datasource when initializes from visualization trigger', async () => { - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + initialContext: { + indexPatternId: '1', + fieldName: 'test', + }, + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockDatasource.getDatasourceSuggestionsForVisualizeField).toHaveBeenCalled(); }); it('should fetch suggestions of currently active datasource', async () => { - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockDatasource.getDatasourceSuggestionsFromCurrentState).toHaveBeenCalled(); expect(mockDatasource2.getDatasourceSuggestionsFromCurrentState).not.toHaveBeenCalled(); @@ -1046,24 +985,20 @@ describe('editor_frame', () => { }, ]); - await act(async () => { - mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }, + + ExpressionRenderer: expressionRendererMock, + }; + await mountWithProvider(, props.plugins.data); expect(mockVisualization.getSuggestions).toHaveBeenCalled(); expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); @@ -1072,71 +1007,66 @@ describe('editor_frame', () => { let instance: ReactWrapper; it('should display top 5 suggestions in descending order', async () => { mockDatasource.getLayers.mockReturnValue(['first']); - - await act(async () => { - instance = mount( - [ - { - score: 0.1, - state: {}, - title: 'Suggestion6', - previewIcon: 'empty', - }, - { - score: 0.5, - state: {}, - title: 'Suggestion3', - previewIcon: 'empty', - }, - { - score: 0.7, - state: {}, - title: 'Suggestion2', - previewIcon: 'empty', - }, - { - score: 0.8, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - ], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.1, + state: {}, + title: 'Suggestion6', + previewIcon: 'empty', }, - testVis2: { - ...mockVisualization, - getSuggestions: () => [ - { - score: 0.4, - state: {}, - title: 'Suggestion5', - previewIcon: 'empty', - }, - { - score: 0.45, - state: {}, - title: 'Suggestion4', - previewIcon: 'empty', - }, - ], + { + score: 0.5, + state: {}, + title: 'Suggestion3', + previewIcon: 'empty', }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + { + score: 0.7, + state: {}, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + { + score: 0.8, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', + }, + ], + }, + testVis2: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.4, + state: {}, + title: 'Suggestion5', + previewIcon: 'empty', + }, + { + score: 0.45, + state: {}, + title: 'Suggestion4', + previewIcon: 'empty', + }, + ], + }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1159,37 +1089,32 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']); const newDatasourceState = {}; const suggestionVisState = {}; - - await act(async () => { - instance = mount( - [ - { - score: 0.8, - state: suggestionVisState, - title: 'Suggestion1', - previewIcon: 'empty', - }, - ], - }, - testVis2: mockVisualization2, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.8, + state: suggestionVisState, + title: 'Suggestion1', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis2" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1199,7 +1124,8 @@ describe('editor_frame', () => { }); // validation requires to calls this getConfiguration API - expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(5); + // TODO: why so many times? + expect(mockVisualization.getConfiguration).toHaveBeenCalledTimes(10); expect(mockVisualization.getConfiguration).toHaveBeenCalledWith( expect.objectContaining({ state: suggestionVisState, @@ -1216,45 +1142,40 @@ describe('editor_frame', () => { it('should switch to best suggested visualization on field drop', async () => { mockDatasource.getLayers.mockReturnValue(['first']); const suggestionVisState = {}; - - await act(async () => { - instance = mount( - [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.8, - state: suggestionVisState, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.2, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', }, - testVis2: mockVisualization2, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + { + score: 0.8, + state: suggestionVisState, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + testVis2: mockVisualization2, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsForField: () => [generateSuggestion()], + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + }, + }, + + ExpressionRenderer: expressionRendererMock, + }; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1274,63 +1195,58 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first', 'second', 'third']); const suggestionVisState = {}; - await act(async () => { - instance = mount( - [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.6, - state: {}, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.2, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', }, - testVis2: { - ...mockVisualization2, - getSuggestions: () => [ - { - score: 0.8, - state: suggestionVisState, - title: 'Suggestion3', - previewIcon: 'empty', - }, - ], + { + score: 0.6, + state: {}, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { - if (!dragging || dragging.id !== 'draggedField') { - setDragging({ - id: 'draggedField', - humanData: { label: 'draggedField' }, - }); - } - }, + ], + }, + testVis2: { + ...mockVisualization2, + getSuggestions: () => [ + { + score: 0.8, + state: suggestionVisState, + title: 'Suggestion3', + previewIcon: 'empty', }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis2" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsForField: () => [generateSuggestion()], + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { + if (!dragging || dragging.id !== 'draggedField') { + setDragging({ + id: 'draggedField', + humanData: { label: 'draggedField' }, + }); + } + }, + }, + }, + ExpressionRenderer: expressionRendererMock, + } as EditorFrameProps; + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1384,58 +1300,55 @@ describe('editor_frame', () => { ], }; - await act(async () => { - instance = mount( - [ - { - score: 0.2, - state: {}, - title: 'Suggestion1', - previewIcon: 'empty', - }, - { - score: 0.6, - state: {}, - title: 'Suggestion2', - previewIcon: 'empty', - }, - ], - }, - testVis2: { - ...mockVisualization2, - getSuggestions: () => [], + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: { + ...mockVisualization, + getSuggestions: () => [ + { + score: 0.2, + state: {}, + title: 'Suggestion1', + previewIcon: 'empty', }, - testVis3: { - ...mockVisualization3, + { + score: 0.6, + state: {}, + title: 'Suggestion2', + previewIcon: 'empty', }, - }} - datasourceMap={{ - testDatasource: { - ...mockDatasource, - getDatasourceSuggestionsForField: () => [generateSuggestion()], - getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], - getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], - renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { - if (!dragging || dragging.id !== 'draggedField') { - setDragging({ - id: 'draggedField', - humanData: { label: '1' }, - }); - } - }, - }, - }} - initialDatasourceId="testDatasource" - initialVisualizationId="testVis2" - ExpressionRenderer={expressionRendererMock} - /> - ); - }); + ], + }, + testVis2: { + ...mockVisualization2, + getSuggestions: () => [], + }, + testVis3: { + ...mockVisualization3, + }, + }, + datasourceMap: { + testDatasource: { + ...mockDatasource, + getDatasourceSuggestionsForField: () => [generateSuggestion()], + getDatasourceSuggestionsFromCurrentState: () => [generateSuggestion()], + getDatasourceSuggestionsForVisualizeField: () => [generateSuggestion()], + renderDataPanel: (_element, { dragDropContext: { setDragging, dragging } }) => { + if (!dragging || dragging.id !== 'draggedField') { + setDragging({ + id: 'draggedField', + humanData: { label: '1' }, + }); + } + }, + }, + }, + + ExpressionRenderer: expressionRendererMock, + } as EditorFrameProps; + + instance = (await mountWithProvider(, props.plugins.data)).instance; // TODO why is this necessary? instance.update(); @@ -1481,74 +1394,79 @@ describe('editor_frame', () => { })); mockVisualization.initialize.mockReturnValue({ initialState: true }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + onChange, + }; + + let lensStore: LensRootStore = {} as LensRootStore; await act(async () => { - mount( - - ); - expect(onChange).toHaveBeenCalledTimes(0); + const mounted = await mountWithProvider(, props.plugins.data); + lensStore = mounted.lensStore; + expect(lensStore.dispatch).toHaveBeenCalledTimes(0); resolver({}); }); - expect(onChange).toHaveBeenCalledTimes(2); - expect(onChange).toHaveBeenNthCalledWith(1, { - filterableIndexPatterns: ['1'], - doc: { - id: undefined, - description: undefined, - references: [ - { - id: '1', - name: 'index-pattern-0', - type: 'index-pattern', + expect(lensStore.dispatch).toHaveBeenCalledTimes(2); + expect(lensStore.dispatch).toHaveBeenNthCalledWith(1, { + payload: { + indexPatternsForTopNav: [{ id: '1' }], + lastKnownDoc: { + savedObjectId: undefined, + description: undefined, + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], + state: { + visualization: null, // Not yet loaded + datasourceStates: { testDatasource: {} }, + query: { query: '', language: 'lucene' }, + filters: [], }, - ], - state: { - visualization: null, // Not yet loaded - datasourceStates: { testDatasource: {} }, - query: { query: '', language: 'lucene' }, - filters: [], + title: '', + type: 'lens', + visualizationType: 'testVis', }, - title: '', - type: 'lens', - visualizationType: 'testVis', }, - isSaveable: false, + type: 'app/onChangeFromEditorFrame', }); - expect(onChange).toHaveBeenLastCalledWith({ - filterableIndexPatterns: ['1'], - doc: { - references: [ - { - id: '1', - name: 'index-pattern-0', - type: 'index-pattern', + expect(lensStore.dispatch).toHaveBeenLastCalledWith({ + payload: { + indexPatternsForTopNav: [{ id: '1' }], + lastKnownDoc: { + references: [ + { + id: '1', + name: 'index-pattern-0', + type: 'index-pattern', + }, + ], + description: undefined, + savedObjectId: undefined, + state: { + visualization: { initialState: true }, // Now loaded + datasourceStates: { testDatasource: {} }, + query: { query: '', language: 'lucene' }, + filters: [], }, - ], - description: undefined, - id: undefined, - state: { - visualization: { initialState: true }, // Now loaded - datasourceStates: { testDatasource: {} }, - query: { query: '', language: 'lucene' }, - filters: [], + title: '', + type: 'lens', + visualizationType: 'testVis', }, - title: '', - type: 'lens', - visualizationType: 'testVis', }, - isSaveable: false, + type: 'app/onChangeFromEditorFrame', }); }); @@ -1561,48 +1479,63 @@ describe('editor_frame', () => { mockDatasource.getLayers.mockReturnValue(['first']); mockVisualization.initialize.mockReturnValue({ initialState: true }); - await act(async () => { - instance = mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + onChange, + }; - expect(onChange).toHaveBeenCalledTimes(2); + const { instance: el, lensStore } = await mountWithProvider( + , + props.plugins.data + ); + instance = el; + + expect(lensStore.dispatch).toHaveBeenCalledTimes(2); mockDatasource.toExpression.mockReturnValue('data expression'); mockVisualization.toExpression.mockReturnValue('vis expression'); - instance.setProps({ query: { query: 'new query', language: 'lucene' } }); + await act(async () => { + lensStore.dispatch(setState({ query: { query: 'new query', language: 'lucene' } })); + }); + instance.update(); - expect(onChange).toHaveBeenCalledTimes(3); - expect(onChange).toHaveBeenNthCalledWith(3, { - filterableIndexPatterns: [], - doc: { - id: undefined, - references: [], - state: { - datasourceStates: { testDatasource: { datasource: '' } }, - visualization: { initialState: true }, - query: { query: 'new query', language: 'lucene' }, - filters: [], + expect(lensStore.dispatch).toHaveBeenCalledTimes(4); + expect(lensStore.dispatch).toHaveBeenNthCalledWith(3, { + payload: { + query: { + language: 'lucene', + query: 'new query', }, - title: '', - type: 'lens', - visualizationType: 'testVis', }, - isSaveable: true, + type: 'app/setState', + }); + expect(lensStore.dispatch).toHaveBeenNthCalledWith(4, { + payload: { + lastKnownDoc: { + savedObjectId: undefined, + references: [], + state: { + datasourceStates: { testDatasource: { datasource: '' } }, + visualization: { initialState: true }, + query: { query: 'new query', language: 'lucene' }, + filters: [], + }, + title: '', + type: 'lens', + visualizationType: 'testVis', + }, + isSaveable: true, + }, + type: 'app/onChangeFromEditorFrame', }); }); @@ -1617,21 +1550,23 @@ describe('editor_frame', () => { })); mockVisualization.initialize.mockReturnValue({ initialState: true }); - await act(async () => { - instance = mount( - - ); - }); + const props = { + ...getDefaultProps(), + visualizationMap: { + testVis: mockVisualization, + }, + datasourceMap: { + testDatasource: mockDatasource, + }, + + ExpressionRenderer: expressionRendererMock, + onChange, + }; + const mounted = await mountWithProvider(, props.plugins.data); + instance = mounted.instance; + const { lensStore } = mounted; - expect(onChange).toHaveBeenCalledTimes(2); + expect(lensStore.dispatch).toHaveBeenCalledTimes(2); await act(async () => { (instance.find(FrameLayout).prop('dataPanel') as ReactElement)!.props.dispatch({ @@ -1643,7 +1578,7 @@ describe('editor_frame', () => { }); }); - expect(onChange).toHaveBeenCalledTimes(3); + expect(lensStore.dispatch).toHaveBeenCalledTimes(3); }); }); }); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 91b59664ada838..4710e03d336bcf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -7,7 +7,10 @@ import React, { useEffect, useReducer, useState, useCallback } from 'react'; import { CoreStart } from 'kibana/public'; +import { isEqual } from 'lodash'; import { PaletteRegistry } from 'src/plugins/charts/public'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import { getAllIndexPatterns } from '../../utils'; import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public'; import { Datasource, FramePublicAPI, Visualization } from '../../types'; import { reducer, getInitialState } from './state_management'; @@ -20,7 +23,6 @@ import { Document } from '../../persistence/saved_object_store'; import { DragDropIdentifier, RootDragDropProvider } from '../../drag_drop'; import { getSavedObjectFormat } from './save'; import { generateId } from '../../id_generator'; -import { Filter, Query, SavedQuery } from '../../../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public'; import { EditorFrameStartPlugins } from '../service'; import { initializeDatasources, createDatasourceLayers } from './state_helpers'; @@ -30,37 +32,45 @@ import { switchToSuggestion, } from './suggestion_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; +import { + useLensSelector, + useLensDispatch, + LensAppState, + DispatchSetState, + onChangeFromEditorFrame, +} from '../../state_management'; export interface EditorFrameProps { - doc?: Document; datasourceMap: Record; visualizationMap: Record; - initialDatasourceId: string | null; - initialVisualizationId: string | null; ExpressionRenderer: ReactExpressionRendererType; palettes: PaletteRegistry; onError: (e: { message: string }) => void; core: CoreStart; plugins: EditorFrameStartPlugins; - dateRange: { - fromDate: string; - toDate: string; - }; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - searchSessionId: string; - onChange: (arg: { - filterableIndexPatterns: string[]; - doc: Document; - isSaveable: boolean; - }) => void; showNoDataPopover: () => void; initialContext?: VisualizeFieldContext; } export function EditorFrame(props: EditorFrameProps) { - const [state, dispatch] = useReducer(reducer, props, getInitialState); + const { + filters, + searchSessionId, + savedQuery, + query, + persistedDoc, + indexPatternsForTopNav, + lastKnownDoc, + activeData, + isSaveable, + resolvedDateRange: dateRange, + } = useLensSelector((state) => state.app); + const [state, dispatch] = useReducer(reducer, { ...props, doc: persistedDoc }, getInitialState); + const dispatchLens = useLensDispatch(); + const dispatchChange: DispatchSetState = useCallback( + (s: Partial) => dispatchLens(onChangeFromEditorFrame(s)), + [dispatchLens] + ); const [visualizeTriggerFieldContext, setVisualizeTriggerFieldContext] = useState( props.initialContext ); @@ -81,7 +91,7 @@ export function EditorFrame(props: EditorFrameProps) { initializeDatasources( props.datasourceMap, state.datasourceStates, - props.doc?.references, + persistedDoc?.references, visualizeTriggerFieldContext, { isFullEditor: true } ) @@ -109,11 +119,11 @@ export function EditorFrame(props: EditorFrameProps) { const framePublicAPI: FramePublicAPI = { datasourceLayers, - activeData: state.activeData, - dateRange: props.dateRange, - query: props.query, - filters: props.filters, - searchSessionId: props.searchSessionId, + activeData, + dateRange, + query, + filters, + searchSessionId, availablePalettes: props.palettes, addNewLayer() { @@ -160,19 +170,19 @@ export function EditorFrame(props: EditorFrameProps) { useEffect( () => { - if (props.doc) { + if (persistedDoc) { dispatch({ type: 'VISUALIZATION_LOADED', doc: { - ...props.doc, + ...persistedDoc, state: { - ...props.doc.state, - visualization: props.doc.visualizationType - ? props.visualizationMap[props.doc.visualizationType].initialize( + ...persistedDoc.state, + visualization: persistedDoc.visualizationType + ? props.visualizationMap[persistedDoc.visualizationType].initialize( framePublicAPI, - props.doc.state.visualization + persistedDoc.state.visualization ) - : props.doc.state.visualization, + : persistedDoc.state.visualization, }, }, }); @@ -184,7 +194,7 @@ export function EditorFrame(props: EditorFrameProps) { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [props.doc] + [persistedDoc] ); // Initialize visualization as soon as all datasources are ready @@ -205,7 +215,7 @@ export function EditorFrame(props: EditorFrameProps) { // Get suggestions for visualize field when all datasources are ready useEffect(() => { - if (allLoaded && visualizeTriggerFieldContext && !props.doc) { + if (allLoaded && visualizeTriggerFieldContext && !persistedDoc) { applyVisualizeFieldSuggestions({ datasourceMap: props.datasourceMap, datasourceStates: state.datasourceStates, @@ -220,6 +230,51 @@ export function EditorFrame(props: EditorFrameProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [allLoaded]); + const getStateToUpdate: ( + arg: { + filterableIndexPatterns: string[]; + doc: Document; + isSaveable: boolean; + }, + oldState: { + isSaveable: boolean; + indexPatternsForTopNav: IndexPattern[]; + persistedDoc?: Document; + lastKnownDoc?: Document; + } + ) => Promise | undefined> = async ( + { filterableIndexPatterns, doc, isSaveable: incomingIsSaveable }, + prevState + ) => { + const batchedStateToUpdate: Partial = {}; + + if (incomingIsSaveable !== prevState.isSaveable) { + batchedStateToUpdate.isSaveable = incomingIsSaveable; + } + + if (!isEqual(prevState.persistedDoc, doc) && !isEqual(prevState.lastKnownDoc, doc)) { + batchedStateToUpdate.lastKnownDoc = doc; + } + const hasIndexPatternsChanged = + prevState.indexPatternsForTopNav.length !== filterableIndexPatterns.length || + filterableIndexPatterns.some( + (id) => !prevState.indexPatternsForTopNav.find((indexPattern) => indexPattern.id === id) + ); + // Update the cached index patterns if the user made a change to any of them + if (hasIndexPatternsChanged) { + const { indexPatterns } = await getAllIndexPatterns( + filterableIndexPatterns, + props.plugins.data.indexPatterns + ); + if (indexPatterns) { + batchedStateToUpdate.indexPatternsForTopNav = indexPatterns; + } + } + if (Object.keys(batchedStateToUpdate).length) { + return batchedStateToUpdate; + } + }; + // The frame needs to call onChange every time its internal state changes useEffect( () => { @@ -232,31 +287,43 @@ export function EditorFrame(props: EditorFrameProps) { return; } - props.onChange( - getSavedObjectFormat({ - activeDatasources: Object.keys(state.datasourceStates).reduce( - (datasourceMap, datasourceId) => ({ - ...datasourceMap, - [datasourceId]: props.datasourceMap[datasourceId], - }), - {} - ), - visualization: activeVisualization, - state, - framePublicAPI, - }) - ); + const savedObjectFormat = getSavedObjectFormat({ + activeDatasources: Object.keys(state.datasourceStates).reduce( + (datasourceMap, datasourceId) => ({ + ...datasourceMap, + [datasourceId]: props.datasourceMap[datasourceId], + }), + {} + ), + visualization: activeVisualization, + state, + framePublicAPI, + }); + + // Frame loader (app or embeddable) is expected to call this when it loads and updates + // This should be replaced with a top-down state + getStateToUpdate(savedObjectFormat, { + isSaveable, + persistedDoc, + indexPatternsForTopNav, + lastKnownDoc, + }).then((batchedStateToUpdate) => { + if (batchedStateToUpdate) { + dispatchChange(batchedStateToUpdate); + } + }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [ activeVisualization, state.datasourceStates, state.visualization, - state.activeData, - props.query, - props.filters, - props.savedQuery, + activeData, + query, + filters, + savedQuery, state.title, + dispatchChange, ] ); @@ -326,9 +393,9 @@ export function EditorFrame(props: EditorFrameProps) { } dispatch={dispatch} core={props.core} - query={props.query} - dateRange={props.dateRange} - filters={props.filters} + query={query} + dateRange={dateRange} + filters={filters} showNoDataPopover={props.showNoDataPopover} dropOntoWorkspace={dropOntoWorkspace} hasSuggestionForField={hasSuggestionForField} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts index 6eec13dd9d7ce0..86a28be65d2b9f 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/save.ts @@ -5,9 +5,8 @@ * 2.0. */ -import _ from 'lodash'; +import { uniq } from 'lodash'; import { SavedObjectReference } from 'kibana/public'; -import { Datatable } from 'src/plugins/expressions'; import { EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; @@ -30,7 +29,6 @@ export function getSavedObjectFormat({ doc: Document; filterableIndexPatterns: string[]; isSaveable: boolean; - activeData: Record | undefined; } { const datasourceStates: Record = {}; const references: SavedObjectReference[] = []; @@ -42,7 +40,7 @@ export function getSavedObjectFormat({ references.push(...savedObjectReferences); }); - const uniqueFilterableIndexPatternIds = _.uniq( + const uniqueFilterableIndexPatternIds = uniq( references.filter(({ type }) => type === 'index-pattern').map(({ id }) => id) ); @@ -77,6 +75,5 @@ export function getSavedObjectFormat({ }, filterableIndexPatterns: uniqueFilterableIndexPatternIds, isSaveable: expression !== null, - activeData: state.activeData, }; } diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts index 5d6dae557dbb8e..af8a9c0a855588 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.test.ts @@ -24,10 +24,7 @@ describe('editor_frame state management', () => { onError: jest.fn(), datasourceMap: { testDatasource: ({} as unknown) as Datasource }, visualizationMap: { testVis: ({ initialize: jest.fn() } as unknown) as Visualization }, - initialDatasourceId: 'testDatasource', - initialVisualizationId: 'testVis', ExpressionRenderer: createExpressionRendererMock(), - onChange: jest.fn(), core: coreMock.createStart(), plugins: { uiActions: uiActionsPluginMock.createStartContract(), @@ -36,11 +33,7 @@ describe('editor_frame state management', () => { charts: chartPluginMock.createStartContract(), }, palettes: chartPluginMock.createPaletteRegistry(), - dateRange: { fromDate: 'now-7d', toDate: 'now' }, - query: { query: '', language: 'lucene' }, - filters: [], showNoDataPopover: jest.fn(), - searchSessionId: 'sessionId', }; }); @@ -101,8 +94,8 @@ describe('editor_frame state management', () => { `); }); - it('should not set active id if no initial visualization is passed in', () => { - const initialState = getInitialState({ ...props, initialVisualizationId: null }); + it('should not set active id if initiated with empty document and visualizationMap is empty', () => { + const initialState = getInitialState({ ...props, visualizationMap: {} }); expect(initialState.visualization.state).toEqual(null); expect(initialState.visualization.activeId).toEqual(null); diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts index 53aba0d6f3f6c1..aa365d1e66d6c5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/state_management.ts @@ -7,7 +7,6 @@ import { EditorFrameProps } from './index'; import { Document } from '../../persistence/saved_object_store'; -import { TableInspectorAdapter } from '../types'; export interface PreviewState { visualization: { @@ -23,7 +22,6 @@ export interface EditorFrameState extends PreviewState { description?: string; stagedPreview?: PreviewState; activeDatasourceId: string | null; - activeData?: TableInspectorAdapter; } export type Action = @@ -35,10 +33,6 @@ export type Action = type: 'UPDATE_TITLE'; title: string; } - | { - type: 'UPDATE_ACTIVE_DATA'; - tables: TableInspectorAdapter; - } | { type: 'UPDATE_STATE'; // Just for diagnostics, so we can determine what action @@ -103,25 +97,27 @@ export function getActiveDatasourceIdFromDoc(doc?: Document) { return null; } - const [initialDatasourceId] = Object.keys(doc.state.datasourceStates); - return initialDatasourceId || null; + const [firstDatasourceFromDoc] = Object.keys(doc.state.datasourceStates); + return firstDatasourceFromDoc || null; } -function getInitialDatasourceId(props: EditorFrameProps) { - return props.initialDatasourceId - ? props.initialDatasourceId - : getActiveDatasourceIdFromDoc(props.doc); -} - -export const getInitialState = (props: EditorFrameProps): EditorFrameState => { +export const getInitialState = ( + params: EditorFrameProps & { doc?: Document } +): EditorFrameState => { const datasourceStates: EditorFrameState['datasourceStates'] = {}; - if (props.doc) { - Object.entries(props.doc.state.datasourceStates).forEach(([datasourceId, state]) => { + const initialDatasourceId = + getActiveDatasourceIdFromDoc(params.doc) || Object.keys(params.datasourceMap)[0] || null; + + const initialVisualizationId = + (params.doc && params.doc.visualizationType) || Object.keys(params.visualizationMap)[0] || null; + + if (params.doc) { + Object.entries(params.doc.state.datasourceStates).forEach(([datasourceId, state]) => { datasourceStates[datasourceId] = { isLoading: true, state }; }); - } else if (props.initialDatasourceId) { - datasourceStates[props.initialDatasourceId] = { + } else if (initialDatasourceId) { + datasourceStates[initialDatasourceId] = { state: null, isLoading: true, }; @@ -130,10 +126,10 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { return { title: '', datasourceStates, - activeDatasourceId: getInitialDatasourceId(props), + activeDatasourceId: initialDatasourceId, visualization: { state: null, - activeId: props.initialVisualizationId, + activeId: initialVisualizationId, }, }; }; @@ -146,11 +142,6 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta return { ...state, title: action.title }; case 'UPDATE_STATE': return action.updater(state); - case 'UPDATE_ACTIVE_DATA': - return { - ...state, - activeData: { ...action.tables }, - }; case 'UPDATE_LAYER': return { ...state, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts index 83b09226265423..bd8f134f59fbb7 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { flatten } from 'lodash'; import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { Datatable } from 'src/plugins/expressions'; @@ -79,7 +79,7 @@ export function getSuggestions({ ); // Collect all table suggestions from available datasources - const datasourceTableSuggestions = _.flatten( + const datasourceTableSuggestions = flatten( datasources.map(([datasourceId, datasource]) => { const datasourceState = datasourceStates[datasourceId].state; let dataSourceSuggestions; @@ -103,9 +103,9 @@ export function getSuggestions({ // Pass all table suggestions to all visualization extensions to get visualization suggestions // and rank them by score - return _.flatten( + return flatten( Object.entries(visualizationMap).map(([visualizationId, visualization]) => - _.flatten( + flatten( datasourceTableSuggestions.map((datasourceSuggestion) => { const table = datasourceSuggestion.table; const currentVisualizationState = diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx index baa9d45a431eaf..1d248c4411023c 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.test.tsx @@ -16,14 +16,14 @@ import { DatasourceMock, createMockFramePublicAPI, } from '../../mocks'; - +import { mockDataPlugin, mountWithProvider } from '../../../mocks'; jest.mock('../../../debounced_component', () => { return { debouncedComponent: (fn: unknown) => fn, }; }); -import { WorkspacePanel, WorkspacePanelProps } from './workspace_panel'; +import { WorkspacePanel } from './workspace_panel'; import { mountWithIntl as mount } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { DragDrop, ChildDragDropProvider } from '../../../drag_drop'; @@ -34,7 +34,6 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ import { uiActionsPluginMock } from '../../../../../../../src/plugins/ui_actions/public/mocks'; import { TriggerContract } from '../../../../../../../src/plugins/ui_actions/public/triggers'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public/embeddable'; -import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; const defaultPermissions: Record>> = { navLinks: { management: true }, @@ -50,24 +49,22 @@ function createCoreStartWithPermissions(newCapabilities = defaultPermissions) { return core; } -function getDefaultProps() { - return { - activeDatasourceId: 'mock', - datasourceStates: {}, - datasourceMap: {}, - framePublicAPI: createMockFramePublicAPI(), - activeVisualizationId: 'vis', - visualizationState: {}, - dispatch: () => {}, - ExpressionRenderer: createExpressionRendererMock(), - core: createCoreStartWithPermissions(), - plugins: { - uiActions: uiActionsPluginMock.createStartContract(), - data: dataPluginMock.createStartContract(), - }, - getSuggestionForField: () => undefined, - }; -} +const defaultProps = { + activeDatasourceId: 'mock', + datasourceStates: {}, + datasourceMap: {}, + framePublicAPI: createMockFramePublicAPI(), + activeVisualizationId: 'vis', + visualizationState: {}, + dispatch: () => {}, + ExpressionRenderer: createExpressionRendererMock(), + core: createCoreStartWithPermissions(), + plugins: { + uiActions: uiActionsPluginMock.createStartContract(), + data: mockDataPlugin(), + }, + getSuggestionForField: () => undefined, +}; describe('workspace_panel', () => { let mockVisualization: jest.Mocked; @@ -78,7 +75,7 @@ describe('workspace_panel', () => { let uiActionsMock: jest.Mocked; let trigger: jest.Mocked; - let instance: ReactWrapper; + let instance: ReactWrapper; beforeEach(() => { // These are used in specific tests to assert function calls @@ -95,50 +92,56 @@ describe('workspace_panel', () => { instance.unmount(); }); - it('should render an explanatory text if no visualization is active', () => { - instance = mount( + it('should render an explanatory text if no visualization is active', async () => { + const mounted = await mountWithProvider( + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should render an explanatory text if the visualization does not produce an expression', () => { - instance = mount( + it('should render an explanatory text if the visualization does not produce an expression', async () => { + const mounted = await mountWithProvider( null }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should render an explanatory text if the datasource does not produce an expression', () => { - instance = mount( + it('should render an explanatory text if the datasource does not produce an expression', async () => { + const mounted = await mountWithProvider( 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="empty-workspace"]')).toHaveLength(2); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should render the resulting expression using the expression renderer', () => { + it('should render the resulting expression using the expression renderer', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -146,9 +149,9 @@ describe('workspace_panel', () => { mockDatasource.toExpression.mockReturnValue('datasource'); mockDatasource.getLayers.mockReturnValue(['first']); - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` "kibana | lens_merge_tables layerIds=\\"first\\" tables={datasource} @@ -173,16 +179,16 @@ describe('workspace_panel', () => { `); }); - it('should execute a trigger on expression event', () => { + it('should execute a trigger on expression event', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; mockDatasource.toExpression.mockReturnValue('datasource'); mockDatasource.getLayers.mockReturnValue(['first']); - const props = getDefaultProps(); + const props = defaultProps; - instance = mount( + const mounted = await mountWithProvider( { }} ExpressionRenderer={expressionRendererMock} plugins={{ ...props.plugins, uiActions: uiActionsMock }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; const onEvent = expressionRendererMock.mock.calls[0][0].onEvent!; @@ -212,7 +220,7 @@ describe('workspace_panel', () => { expect(trigger.exec).toHaveBeenCalledWith({ data: eventData }); }); - it('should push add current data table to state on data$ emitting value', () => { + it('should push add current data table to state on data$ emitting value', async () => { const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, @@ -221,9 +229,9 @@ describe('workspace_panel', () => { mockDatasource.getLayers.mockReturnValue(['first']); const dispatch = jest.fn(); - instance = mount( + const mounted = await mountWithProvider( { }} dispatch={dispatch} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; + const onData = expressionRendererMock.mock.calls[0][0].onData$!; const tableData = { table1: { columns: [], rows: [] } }; onData(undefined, { tables: { tables: tableData } }); - expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_ACTIVE_DATA', tables: tableData }); + expect(mounted.lensStore.dispatch).toHaveBeenCalledWith({ + type: 'app/onActiveDataChange', + payload: { activeData: tableData }, + }); }); - it('should include data fetching for each layer in the expression', () => { + it('should include data fetching for each layer in the expression', async () => { const mockDatasource2 = createMockDatasource('a'); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { @@ -263,9 +277,9 @@ describe('workspace_panel', () => { mockDatasource2.toExpression.mockReturnValue('datasource2'); mockDatasource2.getLayers.mockReturnValue(['second', 'third']); - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; const ast = fromExpression(instance.find(expressionRendererMock).prop('expression') as string); @@ -341,9 +357,9 @@ describe('workspace_panel', () => { expressionRendererMock = jest.fn((_arg) => ); await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -392,9 +410,9 @@ describe('workspace_panel', () => { expressionRendererMock = jest.fn((_arg) => ); await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -434,16 +454,16 @@ describe('workspace_panel', () => { expect(expressionRendererMock).toHaveBeenCalledTimes(2); }); - it('should show an error message if there are missing indexpatterns in the visualization', () => { + it('should show an error message if there are missing indexpatterns in the visualization', async () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource.checkIntegrity.mockReturnValue(['a']); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="missing-refs-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should not show the management action in case of missing indexpattern and no navigation permissions', () => { + it('should not show the management action in case of missing indexpattern and no navigation permissions', async () => { mockDatasource.getLayers.mockReturnValue(['first']); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { navLinks: { management: false }, management: { kibana: { indexPatterns: true } }, })} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect( instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists() ).toBeFalsy(); }); - it('should not show the management action in case of missing indexpattern and no indexPattern specific permissions', () => { + it('should not show the management action in case of missing indexpattern and no indexPattern specific permissions', async () => { mockDatasource.getLayers.mockReturnValue(['first']); const framePublicAPI = createMockFramePublicAPI(); framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { navLinks: { management: true }, management: { kibana: { indexPatterns: false } }, })} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect( instance.find('[data-test-subj="configuration-failure-reconfigure-indexpatterns"]').exists() ).toBeFalsy(); }); - it('should show an error message if validation on datasource does not pass', () => { + it('should show an error message if validation on datasource does not pass', async () => { mockDatasource.getErrorMessages.mockReturnValue([ { shortMessage: 'An error occurred', longMessage: 'An long description here' }, ]); @@ -550,9 +576,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should show an error message if validation on visualization does not pass', () => { + it('should show an error message if validation on visualization does not pass', async () => { mockDatasource.getErrorMessages.mockReturnValue(undefined); mockDatasource.getLayers.mockReturnValue(['first']); mockVisualization.getErrorMessages.mockReturnValue([ @@ -585,9 +613,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: mockVisualization, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="configuration-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should show an error message if validation on both datasource and visualization do not pass', () => { + it('should show an error message if validation on both datasource and visualization do not pass', async () => { mockDatasource.getErrorMessages.mockReturnValue([ { shortMessage: 'An error occurred', longMessage: 'An long description here' }, ]); @@ -622,9 +652,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: mockVisualization, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; // EuiFlexItem duplicates internally the attribute, so we need to filter only the most inner one here expect( @@ -648,7 +680,7 @@ describe('workspace_panel', () => { expect(instance.find(expressionRendererMock)).toHaveLength(0); }); - it('should show an error message if the expression fails to parse', () => { + it('should show an error message if the expression fails to parse', async () => { mockDatasource.toExpression.mockReturnValue('|||'); mockDatasource.getLayers.mockReturnValue(['first']); const framePublicAPI = createMockFramePublicAPI(); @@ -656,9 +688,9 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; - instance = mount( + const mounted = await mountWithProvider( { visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; expect(instance.find('[data-test-subj="expression-failure"]').exists()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); @@ -688,9 +722,9 @@ describe('workspace_panel', () => { }; await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -727,9 +763,9 @@ describe('workspace_panel', () => { }; await act(async () => { - instance = mount( + const mounted = await mountWithProvider( { vis: { ...mockVisualization, toExpression: () => 'vis' }, }} ExpressionRenderer={expressionRendererMock} - /> + />, + defaultProps.plugins.data ); + instance = mounted.instance; }); instance.update(); @@ -791,7 +829,7 @@ describe('workspace_panel', () => { dropTargetsByOrder={undefined} > { ); } - it('should immediately transition if exactly one suggestion is returned', () => { + it('should immediately transition if exactly one suggestion is returned', async () => { mockGetSuggestionForField.mockReturnValue({ visualizationId: 'vis', datasourceState: {}, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 3d5d9a6d84d811..94065f316340cb 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -54,6 +54,7 @@ import { DropIllustration } from '../../../assets/drop_illustration'; import { getOriginalRequestErrorMessages } from '../../error_helper'; import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers'; import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common'; +import { onActiveDataChange, useLensDispatch } from '../../../state_management'; export interface WorkspacePanelProps { activeVisualizationId: string | null; @@ -428,16 +429,15 @@ export const VisualizationWrapper = ({ ] ); + const dispatchLens = useLensDispatch(); + const onData$ = useCallback( (data: unknown, inspectorAdapters?: Partial) => { if (inspectorAdapters && inspectorAdapters.tables) { - dispatch({ - type: 'UPDATE_ACTIVE_DATA', - tables: inspectorAdapters.tables.tables, - }); + dispatchLens(onActiveDataChange({ activeData: { ...inspectorAdapters.tables.tables } })); } }, - [dispatch] + [dispatchLens] ); if (localState.configurationValidationError?.length) { diff --git a/x-pack/plugins/lens/public/editor_frame_service/service.tsx b/x-pack/plugins/lens/public/editor_frame_service/service.tsx index f6500596ce5a0e..62274df23e837b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/service.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/service.tsx @@ -126,26 +126,11 @@ export class EditorFrameService { collectAsyncDefinitions(this.visualizations), ]); - const firstDatasourceId = Object.keys(resolvedDatasources)[0]; - const firstVisualizationId = Object.keys(resolvedVisualizations)[0]; - - const { EditorFrame, getActiveDatasourceIdFromDoc } = await import('../async_services'); - + const { EditorFrame } = await import('../async_services'); const palettes = await plugins.charts.palettes.getPalettes(); return { - EditorFrameContainer: ({ - doc, - onError, - dateRange, - query, - filters, - savedQuery, - onChange, - showNoDataPopover, - initialContext, - searchSessionId, - }) => { + EditorFrameContainer: ({ onError, showNoDataPopover, initialContext }) => { return (
); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index c0a502df142346..81dff1da578094 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -359,7 +359,7 @@ describe('IndexPattern Data Source', () => { true, ], "id": Array [ - "col1", + "0", ], "schema": Array [ "metric", @@ -388,7 +388,7 @@ describe('IndexPattern Data Source', () => { "timestamp", ], "id": Array [ - "col2", + "1", ], "interval": Array [ "1d", @@ -442,7 +442,7 @@ describe('IndexPattern Data Source', () => { Object { "arguments": Object { "idMap": Array [ - "{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", + "{\\"col-0-0\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-1\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}", ], }, "function": "lens_rename_columns", @@ -563,7 +563,7 @@ describe('IndexPattern Data Source', () => { "{\\"language\\":\\"kuery\\",\\"query\\":\\"bytes > 5\\"}", ], "id": Array [ - "col1-filter", + "0-filter", ], "schema": Array [ "bucket", @@ -585,7 +585,7 @@ describe('IndexPattern Data Source', () => { true, ], "id": Array [ - "col1-metric", + "0-metric", ], "schema": Array [ "metric", @@ -602,7 +602,7 @@ describe('IndexPattern Data Source', () => { true, ], "id": Array [ - "col1", + "0", ], "schema": Array [ "metric", @@ -795,9 +795,9 @@ describe('IndexPattern Data Source', () => { const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; expect(ast.chain[0].arguments.metricsAtAllLevels).toEqual([false]); expect(JSON.parse(ast.chain[1].arguments.idMap[0] as string)).toEqual({ - 'col-0-bucket1': expect.any(Object), - 'col-1-bucket2': expect.any(Object), - 'col-2-metric': expect.any(Object), + 'col-0-0': expect.objectContaining({ id: 'bucket1' }), + 'col-1-1': expect.objectContaining({ id: 'bucket2' }), + 'col-2-2': expect.objectContaining({ id: 'metric' }), }); }); @@ -919,7 +919,7 @@ describe('IndexPattern Data Source', () => { const ast = indexPatternDatasource.toExpression(state, 'first') as Ast; expect(JSON.parse(ast.chain[1].arguments.idMap[0] as string)).toEqual({ - 'col-0-col1': expect.objectContaining({ + 'col-0-0': expect.objectContaining({ id: 'col1', }), }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx index eaaf13171124b0..92741b9b5ed09d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/date_histogram.test.tsx @@ -205,7 +205,8 @@ describe('date_histogram', () => { 'col1', indexPattern1, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -258,7 +259,8 @@ describe('date_histogram', () => { ]), }, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts index a7402bc13c0a88..6772432664d8cd 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/index.ts @@ -355,7 +355,8 @@ interface FieldBasedOperationDefinition { columnId: string, indexPattern: IndexPattern, layer: IndexPatternLayer, - uiSettings: IUiSettingsClient + uiSettings: IUiSettingsClient, + orderedColumnIds: string[] ) => ExpressionAstFunction; /** * Validate that the operation has the right preconditions in the state. For example: diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx index 280cfe9471c9d0..15ce3bdcd0b0f5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/last_value.test.tsx @@ -75,7 +75,8 @@ describe('last_value', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 59da0f6f7bcdea..2b7104112d63eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -123,7 +123,8 @@ describe('percentile', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index cfbe0f8903dafb..295f988c6e3906 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -172,7 +172,8 @@ describe('ranges', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toMatchInlineSnapshot(` Object { @@ -219,7 +220,8 @@ describe('ranges', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( @@ -240,7 +242,8 @@ describe('ranges', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( @@ -261,7 +264,8 @@ describe('ranges', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect((esAggsFn as { arguments: unknown }).arguments).toEqual( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index d226fe6f2a7452..52adf83752363e 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -132,14 +132,16 @@ export const termsOperation: OperationDefinition { + toEsAggsFn: (column, columnId, _indexPattern, layer, uiSettings, orderedColumnIds) => { return buildExpressionFunction('aggTerms', { id: columnId, enabled: true, schema: 'segment', field: column.sourceField, orderBy: - column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId, + column.params.orderBy.type === 'alphabetical' + ? '_key' + : String(orderedColumnIds.indexOf(column.params.orderBy.columnId)), order: column.params.orderDirection, size: column.params.size, otherBucket: Boolean(column.params.otherBucket), diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx index b094d3f0ff5cd7..aab957c8ecebe4 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/terms.test.tsx @@ -71,7 +71,8 @@ describe('terms', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( expect.objectContaining({ @@ -95,7 +96,8 @@ describe('terms', () => { 'col1', {} as IndexPattern, layer, - uiSettingsMock + uiSettingsMock, + [] ); expect(esAggsFn).toEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts index 4905bd75d64987..430e139a85ccae 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/to_expression.ts @@ -77,27 +77,29 @@ function getExpressionForLayer( } }); - esAggEntries.forEach(([colId, col]) => { + const orderedColumnIds = esAggEntries.map(([colId]) => colId); + esAggEntries.forEach(([colId, col], index) => { const def = operationDefinitionMap[col.operationType]; if (def.input !== 'fullReference' && def.input !== 'managedReference') { const wrapInFilter = Boolean(def.filterable && col.filter); let aggAst = def.toEsAggsFn( col, - wrapInFilter ? `${colId}-metric` : colId, + wrapInFilter ? `${index}-metric` : String(index), indexPattern, layer, - uiSettings + uiSettings, + orderedColumnIds ); if (wrapInFilter) { aggAst = buildExpressionFunction( 'aggFilteredMetric', { - id: colId, + id: String(index), enabled: true, schema: 'metric', customBucket: buildExpression([ buildExpressionFunction('aggFilter', { - id: `${colId}-filter`, + id: `${index}-filter`, enabled: true, schema: 'bucket', filter: JSON.stringify(col.filter), @@ -121,7 +123,7 @@ function getExpressionForLayer( return null; } const idMap = esAggEntries.reduce((currentIdMap, [colId, column], index) => { - const esAggsId = `col-${index}-${colId}`; + const esAggsId = `col-${index}-${index}`; return { ...currentIdMap, [esAggsId]: { diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index c1f885d167659d..473c170aef2948 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -6,8 +6,35 @@ */ import React from 'react'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { ReactWrapper } from 'enzyme'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { mountWithIntl as mount } from '@kbn/test/jest'; +import { Observable, Subject } from 'rxjs'; +import { coreMock } from 'src/core/public/mocks'; +import moment from 'moment'; +import { Provider } from 'react-redux'; +import { act } from 'react-dom/test-utils'; import { LensPublicStart } from '.'; import { visualizationTypes } from './xy_visualization/types'; +import { navigationPluginMock } from '../../../../src/plugins/navigation/public/mocks'; +import { LensAppServices } from './app_plugin/types'; +import { DOC_TYPE } from '../common'; +import { DataPublicPluginStart, esFilters, UI_SETTINGS } from '../../../../src/plugins/data/public'; +import { + LensByValueInput, + LensSavedObjectAttributes, + LensByReferenceInput, +} from './editor_frame_service/embeddable/embeddable'; +import { + mockAttributeService, + createEmbeddableStateTransferMock, +} from '../../../../src/plugins/embeddable/public/mocks'; +import { LensAttributeService } from './lens_attribute_service'; +import { EmbeddableStateTransfer } from '../../../../src/plugins/embeddable/public'; + +import { makeConfigureStore, getPreloadedState, LensAppState } from './state_management/index'; +import { getResolvedDateRange } from './utils'; export type Start = jest.Mocked; @@ -26,3 +53,252 @@ const createStartContract = (): Start => { export const lensPluginMock = { createStartContract, }; + +export const defaultDoc = ({ + savedObjectId: '1234', + title: 'An extremely cool default document!', + expression: 'definitely a valid expression', + state: { + query: 'kuery', + filters: [{ query: { match_phrase: { src: 'test' } } }], + }, + references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], +} as unknown) as Document; + +export function createMockTimefilter() { + const unsubscribe = jest.fn(); + + let timeFilter = { from: 'now-7d', to: 'now' }; + let subscriber: () => void; + return { + getTime: jest.fn(() => timeFilter), + setTime: jest.fn((newTimeFilter) => { + timeFilter = newTimeFilter; + if (subscriber) { + subscriber(); + } + }), + getTimeUpdate$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + calculateBounds: jest.fn(() => ({ + min: moment('2021-01-10T04:00:00.000Z'), + max: moment('2021-01-10T08:00:00.000Z'), + })), + getBounds: jest.fn(() => timeFilter), + getRefreshInterval: () => {}, + getRefreshIntervalDefaults: () => {}, + getAutoRefreshFetch$: () => new Observable(), + }; +} + +export function mockDataPlugin(sessionIdSubject = new Subject()) { + function createMockSearchService() { + let sessionIdCounter = 1; + return { + session: { + start: jest.fn(() => `sessionId-${sessionIdCounter++}`), + clear: jest.fn(), + getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), + getSession$: jest.fn(() => sessionIdSubject.asObservable()), + }, + }; + } + + function createMockFilterManager() { + const unsubscribe = jest.fn(); + + let subscriber: () => void; + let filters: unknown = []; + + return { + getUpdates$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + setFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + getFilters: () => filters, + getGlobalFilters: () => { + // @ts-ignore + return filters.filter(esFilters.isFilterPinned); + }, + removeAll: () => { + filters = []; + subscriber(); + }, + }; + } + + function createMockQueryString() { + return { + getQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + setQuery: jest.fn(), + getDefaultQuery: jest.fn(() => ({ query: '', language: 'lucene' })), + }; + } + return ({ + query: { + filterManager: createMockFilterManager(), + timefilter: { + timefilter: createMockTimefilter(), + }, + queryString: createMockQueryString(), + state$: new Observable(), + }, + indexPatterns: { + get: jest.fn((id) => { + return new Promise((resolve) => resolve({ id })); + }), + }, + search: createMockSearchService(), + nowProvider: { + get: jest.fn(), + }, + } as unknown) as DataPublicPluginStart; +} + +export function makeDefaultServices( + sessionIdSubject = new Subject(), + doc = defaultDoc +): jest.Mocked { + const core = coreMock.createStart({ basePath: '/testbasepath' }); + core.uiSettings.get.mockImplementation( + jest.fn((type) => { + if (type === UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS) { + return { from: 'now-7d', to: 'now' }; + } else if (type === UI_SETTINGS.SEARCH_QUERY_LANGUAGE) { + return 'kuery'; + } else if (type === 'state:storeInSessionStorage') { + return false; + } else { + return []; + } + }) + ); + + const navigationStartMock = navigationPluginMock.createStartContract(); + + jest.spyOn(navigationStartMock.ui.TopNavMenu.prototype, 'constructor').mockImplementation(() => { + return
; + }); + + function makeAttributeService(): LensAttributeService { + const attributeServiceMock = mockAttributeService< + LensSavedObjectAttributes, + LensByValueInput, + LensByReferenceInput + >( + DOC_TYPE, + { + saveMethod: jest.fn(), + unwrapMethod: jest.fn(), + checkForDuplicateTitle: jest.fn(), + }, + core + ); + + attributeServiceMock.unwrapAttributes = jest.fn().mockResolvedValue(doc); + attributeServiceMock.wrapAttributes = jest.fn().mockResolvedValue({ + savedObjectId: ((doc as unknown) as LensByReferenceInput).savedObjectId, + }); + + return attributeServiceMock; + } + + return { + http: core.http, + chrome: core.chrome, + overlays: core.overlays, + uiSettings: core.uiSettings, + navigation: navigationStartMock, + notifications: core.notifications, + attributeService: makeAttributeService(), + savedObjectsClient: core.savedObjects.client, + dashboardFeatureFlag: { allowByValueEmbeddables: false }, + stateTransfer: createEmbeddableStateTransferMock() as EmbeddableStateTransfer, + getOriginatingAppName: jest.fn(() => 'defaultOriginatingApp'), + application: { + ...core.application, + capabilities: { + ...core.application.capabilities, + visualize: { save: true, saveQuery: true, show: true }, + }, + getUrlForApp: jest.fn((appId: string) => `/testbasepath/app/${appId}#/`), + }, + data: mockDataPlugin(sessionIdSubject), + storage: { + get: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + clear: jest.fn(), + }, + }; +} + +export function mockLensStore({ + data, + storePreloadedState, +}: { + data: DataPublicPluginStart; + storePreloadedState?: Partial; +}) { + const lensStore = makeConfigureStore( + getPreloadedState({ + query: data.query.queryString.getQuery(), + filters: data.query.filterManager.getGlobalFilters(), + searchSessionId: data.search.session.start(), + resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), + ...storePreloadedState, + }), + { + data, + } + ); + + const origDispatch = lensStore.dispatch; + lensStore.dispatch = jest.fn(origDispatch); + return lensStore; +} + +export const mountWithProvider = async ( + component: React.ReactElement, + data: DataPublicPluginStart, + storePreloadedState?: Partial, + extraWrappingComponent?: React.FC<{ + children: React.ReactNode; + }> +) => { + const lensStore = mockLensStore({ data, storePreloadedState }); + + const wrappingComponent: React.FC<{ + children: React.ReactNode; + }> = ({ children }) => { + if (extraWrappingComponent) { + return extraWrappingComponent({ + children: {children}, + }); + } + return {children}; + }; + + let instance: ReactWrapper = {} as ReactWrapper; + + await act(async () => { + instance = mount(component, ({ + wrappingComponent, + } as unknown) as ReactWrapper); + }); + return { instance, lensStore }; +}; diff --git a/x-pack/plugins/lens/public/shared_components/debounced_value.ts b/x-pack/plugins/lens/public/shared_components/debounced_value.ts index 5447384ce38ea5..1f8ba0fa765b2e 100644 --- a/x-pack/plugins/lens/public/shared_components/debounced_value.ts +++ b/x-pack/plugins/lens/public/shared_components/debounced_value.ts @@ -6,7 +6,7 @@ */ import { useState, useMemo, useEffect, useRef } from 'react'; -import _ from 'lodash'; +import { debounce } from 'lodash'; /** * Debounces value changes and updates inputValue on root state changes if no debounced changes @@ -27,7 +27,7 @@ export const useDebouncedValue = ({ const initialValue = useRef(value); const onChangeDebounced = useMemo(() => { - const callback = _.debounce((val: T) => { + const callback = debounce((val: T) => { onChange(val); unflushedChanges.current = false; }, 256); diff --git a/x-pack/plugins/lens/public/state_management/app_slice.ts b/x-pack/plugins/lens/public/state_management/app_slice.ts new file mode 100644 index 00000000000000..29d5b0bee843f6 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/app_slice.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 { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; +import { LensAppState } from './types'; + +export const initialState: LensAppState = { + searchSessionId: '', + filters: [], + query: { language: 'kuery', query: '' }, + resolvedDateRange: { fromDate: '', toDate: '' }, + + indexPatternsForTopNav: [], + isSaveable: false, + isAppLoading: false, + isLinkedToOriginatingApp: false, +}; + +export const appSlice = createSlice({ + name: 'app', + initialState, + reducers: { + setState: (state, { payload }: PayloadAction>) => { + return { + ...state, + ...payload, + }; + }, + onChangeFromEditorFrame: (state, { payload }: PayloadAction>) => { + return { + ...state, + ...payload, + }; + }, + onActiveDataChange: (state, { payload }: PayloadAction>) => { + if (!isEqual(state.activeData, payload?.activeData)) { + return { + ...state, + ...payload, + }; + } + return state; + }, + navigateAway: (state) => state, + }, +}); + +export const reducer = { + app: appSlice.reducer, +}; diff --git a/x-pack/plugins/lens/public/state_management/external_context_middleware.ts b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts new file mode 100644 index 00000000000000..35d0f7cf197edd --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { delay, finalize, switchMap, tap } from 'rxjs/operators'; +import _, { debounce } from 'lodash'; +import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit'; +import { trackUiEvent } from '../lens_ui_telemetry'; + +import { + waitUntilNextSessionCompletes$, + DataPublicPluginStart, +} from '../../../../../src/plugins/data/public'; +import { setState, LensGetState, LensDispatch } from '.'; +import { LensAppState } from './types'; +import { getResolvedDateRange } from '../utils'; + +export const externalContextMiddleware = (data: DataPublicPluginStart) => ( + store: MiddlewareAPI +) => { + const unsubscribeFromExternalContext = subscribeToExternalContext( + data, + store.getState, + store.dispatch + ); + return (next: Dispatch) => (action: PayloadAction>) => { + if (action.type === 'app/navigateAway') { + unsubscribeFromExternalContext(); + } + next(action); + }; +}; + +function subscribeToExternalContext( + data: DataPublicPluginStart, + getState: LensGetState, + dispatch: LensDispatch +) { + const { query: queryService, search } = data; + const { filterManager } = queryService; + + const dispatchFromExternal = (searchSessionId = search.session.start()) => { + const globalFilters = filterManager.getFilters(); + const filters = _.isEqual(getState().app.filters, globalFilters) + ? null + : { filters: globalFilters }; + dispatch( + setState({ + searchSessionId, + ...filters, + resolvedDateRange: getResolvedDateRange(queryService.timefilter.timefilter), + }) + ); + }; + + const debounceDispatchFromExternal = debounce(dispatchFromExternal, 100); + + const sessionSubscription = search.session + .getSession$() + // wait for a tick to filter/timerange subscribers the chance to update the session id in the state + .pipe(delay(0)) + // then update if it didn't get updated yet + .subscribe((newSessionId?: string) => { + if (newSessionId && getState().app.searchSessionId !== newSessionId) { + debounceDispatchFromExternal(newSessionId); + } + }); + + const filterSubscription = filterManager.getUpdates$().subscribe({ + next: () => { + debounceDispatchFromExternal(); + trackUiEvent('app_filters_updated'); + }, + }); + + const timeSubscription = data.query.timefilter.timefilter.getTimeUpdate$().subscribe({ + next: () => { + debounceDispatchFromExternal(); + }, + }); + + const autoRefreshSubscription = data.query.timefilter.timefilter + .getAutoRefreshFetch$() + .pipe( + tap(() => { + debounceDispatchFromExternal(); + }), + switchMap((done) => + // best way in lens to estimate that all panels are updated is to rely on search session service state + waitUntilNextSessionCompletes$(search.session).pipe(finalize(done)) + ) + ) + .subscribe(); + return () => { + filterSubscription.unsubscribe(); + timeSubscription.unsubscribe(); + autoRefreshSubscription.unsubscribe(); + sessionSubscription.unsubscribe(); + }; +} diff --git a/x-pack/plugins/lens/public/state_management/index.ts b/x-pack/plugins/lens/public/state_management/index.ts new file mode 100644 index 00000000000000..429978e60756b8 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/index.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { configureStore, DeepPartial, getDefaultMiddleware } from '@reduxjs/toolkit'; +import logger from 'redux-logger'; +import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'; +import { appSlice, initialState } from './app_slice'; +import { timeRangeMiddleware } from './time_range_middleware'; +import { externalContextMiddleware } from './external_context_middleware'; + +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { LensAppState, LensState } from './types'; +export * from './types'; + +export const reducer = { + app: appSlice.reducer, +}; + +export const { + setState, + navigateAway, + onChangeFromEditorFrame, + onActiveDataChange, +} = appSlice.actions; + +export const getPreloadedState = (initializedState: Partial) => { + const state = { + app: { + ...initialState, + ...initializedState, + }, + } as DeepPartial; + return state; +}; + +type PreloadedState = ReturnType; + +export const makeConfigureStore = ( + preloadedState: PreloadedState, + { data }: { data: DataPublicPluginStart } +) => { + const middleware = [ + ...getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [ + 'app/setState', + 'app/onChangeFromEditorFrame', + 'app/onActiveDataChange', + 'app/navigateAway', + ], + }, + }), + timeRangeMiddleware(data), + externalContextMiddleware(data), + ]; + if (process.env.NODE_ENV === 'development') middleware.push(logger); + + return configureStore({ + reducer, + middleware, + preloadedState, + }); +}; + +export type LensRootStore = ReturnType; + +export type LensDispatch = LensRootStore['dispatch']; +export type LensGetState = LensRootStore['getState']; +export type LensRootState = ReturnType; + +export const useLensDispatch = () => useDispatch(); +export const useLensSelector: TypedUseSelectorHook = useSelector; diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts new file mode 100644 index 00000000000000..4145f8ed5e52cf --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.test.ts @@ -0,0 +1,198 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.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 { timeRangeMiddleware } from './time_range_middleware'; + +import { Observable, Subject } from 'rxjs'; +import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public'; +import moment from 'moment'; +import { initialState } from './app_slice'; +import { LensAppState } from './types'; +import { PayloadAction } from '@reduxjs/toolkit'; +import { Document } from '../persistence'; + +const sessionIdSubject = new Subject(); + +function createMockSearchService() { + let sessionIdCounter = 1; + return { + session: { + start: jest.fn(() => `sessionId-${sessionIdCounter++}`), + clear: jest.fn(), + getSessionId: jest.fn(() => `sessionId-${sessionIdCounter}`), + getSession$: jest.fn(() => sessionIdSubject.asObservable()), + }, + }; +} + +function createMockFilterManager() { + const unsubscribe = jest.fn(); + + let subscriber: () => void; + let filters: unknown = []; + + return { + getUpdates$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + setFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + setAppFilters: jest.fn((newFilters: unknown[]) => { + filters = newFilters; + if (subscriber) subscriber(); + }), + getFilters: () => filters, + getGlobalFilters: () => { + // @ts-ignore + return filters.filter(esFilters.isFilterPinned); + }, + removeAll: () => { + filters = []; + subscriber(); + }, + }; +} + +function createMockQueryString() { + return { + getQuery: jest.fn(() => ({ query: '', language: 'kuery' })), + setQuery: jest.fn(), + getDefaultQuery: jest.fn(() => ({ query: '', language: 'kuery' })), + }; +} + +function createMockTimefilter() { + const unsubscribe = jest.fn(); + + let timeFilter = { from: 'now-7d', to: 'now' }; + let subscriber: () => void; + return { + getTime: jest.fn(() => timeFilter), + setTime: jest.fn((newTimeFilter) => { + timeFilter = newTimeFilter; + if (subscriber) { + subscriber(); + } + }), + getTimeUpdate$: () => ({ + subscribe: ({ next }: { next: () => void }) => { + subscriber = next; + return unsubscribe; + }, + }), + calculateBounds: jest.fn(() => ({ + min: moment('2021-01-10T04:00:00.000Z'), + max: moment('2021-01-10T08:00:00.000Z'), + })), + getBounds: jest.fn(() => timeFilter), + getRefreshInterval: () => {}, + getRefreshIntervalDefaults: () => {}, + getAutoRefreshFetch$: () => new Observable(), + }; +} + +function makeDefaultData(): jest.Mocked { + return ({ + query: { + filterManager: createMockFilterManager(), + timefilter: { + timefilter: createMockTimefilter(), + }, + queryString: createMockQueryString(), + state$: new Observable(), + }, + indexPatterns: { + get: jest.fn((id) => { + return new Promise((resolve) => resolve({ id })); + }), + }, + search: createMockSearchService(), + nowProvider: { + get: jest.fn(), + }, + } as unknown) as DataPublicPluginStart; +} + +const createMiddleware = (data: DataPublicPluginStart) => { + const middleware = timeRangeMiddleware(data); + const store = { + getState: jest.fn(() => ({ app: initialState })), + dispatch: jest.fn(), + }; + const next = jest.fn(); + + const invoke = (action: PayloadAction>) => middleware(store)(next)(action); + + return { store, next, invoke }; +}; + +describe('timeRangeMiddleware', () => { + describe('time update', () => { + it('does update the searchSessionId when the state changes and too much time passed', () => { + const data = makeDefaultData(); + (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 30000)); + (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + (data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ + min: moment(Date.now() - 100000), + max: moment(Date.now() - 30000), + }); + const { next, invoke, store } = createMiddleware(data); + const action = { + type: 'app/setState', + payload: { lastKnownDoc: ('new' as unknown) as Document }, + }; + invoke(action); + expect(store.dispatch).toHaveBeenCalledWith({ + payload: { + resolvedDateRange: { + fromDate: '2021-01-10T04:00:00.000Z', + toDate: '2021-01-10T08:00:00.000Z', + }, + searchSessionId: 'sessionId-1', + }, + type: 'app/setState', + }); + expect(next).toHaveBeenCalledWith(action); + }); + it('does not update the searchSessionId when the state changes and too little time has passed', () => { + const data = makeDefaultData(); + // time range is 100,000ms ago to 300ms ago (that's a lag of .3 percent, not enough to trigger a session update) + (data.nowProvider.get as jest.Mock).mockReturnValue(new Date(Date.now() - 300)); + (data.query.timefilter.timefilter.getTime as jest.Mock).mockReturnValue({ + from: 'now-2m', + to: 'now', + }); + (data.query.timefilter.timefilter.getBounds as jest.Mock).mockReturnValue({ + min: moment(Date.now() - 100000), + max: moment(Date.now() - 300), + }); + const { next, invoke, store } = createMiddleware(data); + const action = { + type: 'app/setState', + payload: { lastKnownDoc: ('new' as unknown) as Document }, + }; + invoke(action); + expect(store.dispatch).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalledWith(action); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/state_management/time_range_middleware.ts b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts new file mode 100644 index 00000000000000..a6c868be605650 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/time_range_middleware.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEqual } from 'lodash'; +import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit'; +import moment from 'moment'; + +import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; +import { setState, LensDispatch } from '.'; +import { LensAppState } from './types'; +import { getResolvedDateRange, containsDynamicMath, TIME_LAG_PERCENTAGE_LIMIT } from '../utils'; + +export const timeRangeMiddleware = (data: DataPublicPluginStart) => (store: MiddlewareAPI) => { + return (next: Dispatch) => (action: PayloadAction>) => { + // if document was modified or sessionId check if too much time passed to update searchSessionId + if ( + action.payload?.lastKnownDoc && + !isEqual(action.payload?.lastKnownDoc, store.getState().app.lastKnownDoc) + ) { + updateTimeRange(data, store.dispatch); + } + next(action); + }; +}; +function updateTimeRange(data: DataPublicPluginStart, dispatch: LensDispatch) { + const timefilter = data.query.timefilter.timefilter; + const unresolvedTimeRange = timefilter.getTime(); + if ( + !containsDynamicMath(unresolvedTimeRange.from) && + !containsDynamicMath(unresolvedTimeRange.to) + ) { + return; + } + + const { min, max } = timefilter.getBounds(); + + if (!min || !max) { + // bounds not fully specified, bailing out + return; + } + + // calculate length of currently configured range in ms + const timeRangeLength = moment.duration(max.diff(min)).asMilliseconds(); + + // calculate lag of managed "now" for date math + const nowDiff = Date.now() - data.nowProvider.get().valueOf(); + + // if the lag is signifcant, start a new session to clear the cache + if (nowDiff > timeRangeLength * TIME_LAG_PERCENTAGE_LIMIT) { + dispatch( + setState({ + searchSessionId: data.search.session.start(), + resolvedDateRange: getResolvedDateRange(timefilter), + }) + ); + } +} diff --git a/x-pack/plugins/lens/public/state_management/types.ts b/x-pack/plugins/lens/public/state_management/types.ts new file mode 100644 index 00000000000000..87045d15cc9946 --- /dev/null +++ b/x-pack/plugins/lens/public/state_management/types.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 { Filter, IndexPattern, Query, SavedQuery } from '../../../../../src/plugins/data/public'; +import { Document } from '../persistence'; + +import { TableInspectorAdapter } from '../editor_frame_service/types'; +import { DateRange } from '../../common'; + +export interface LensAppState { + persistedDoc?: Document; + lastKnownDoc?: Document; + + // index patterns used to determine which filters are available in the top nav. + indexPatternsForTopNav: IndexPattern[]; + // Determines whether the lens editor shows the 'save and return' button, and the originating app breadcrumb. + isLinkedToOriginatingApp?: boolean; + isSaveable: boolean; + activeData?: TableInspectorAdapter; + + isAppLoading: boolean; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; + searchSessionId: string; + resolvedDateRange: DateRange; +} + +export type DispatchSetState = ( + state: Partial +) => { + payload: Partial; + type: string; +}; + +export interface LensState { + app: LensAppState; +} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 9cde4eb8a15616..5a632e03f8f36c 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -18,9 +18,8 @@ import { SerializedFieldFormat, } from '../../../../src/plugins/expressions/public'; import { DraggingIdentifier, DragDropIdentifier, DragContextState } from './drag_drop'; -import { Document } from './persistence'; import { DateRange } from '../common'; -import { Query, Filter, SavedQuery, IFieldFormat } from '../../../../src/plugins/data/public'; +import { Query, Filter, IFieldFormat } from '../../../../src/plugins/data/public'; import { VisualizeFieldContext } from '../../../../src/plugins/ui_actions/public'; import { RangeSelectContext, ValueClickContext } from '../../../../src/plugins/embeddable/public'; import { @@ -46,22 +45,7 @@ export interface PublicAPIProps { export interface EditorFrameProps { onError: ErrorCallback; - doc?: Document; - dateRange: DateRange; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; - searchSessionId: string; initialContext?: VisualizeFieldContext; - - // Frame loader (app or embeddable) is expected to call this when it loads and updates - // This should be replaced with a top-down state - onChange: (newState: { - filterableIndexPatterns: string[]; - doc: Document; - isSaveable: boolean; - activeData?: Record; - }) => void; showNoDataPopover: () => void; } export interface EditorFrameInstance { diff --git a/x-pack/plugins/lens/public/utils.ts b/x-pack/plugins/lens/public/utils.ts index 2d8cfee2185fa2..c1aab4c18f5297 100644 --- a/x-pack/plugins/lens/public/utils.ts +++ b/x-pack/plugins/lens/public/utils.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { IndexPattern, IndexPatternsContract, TimefilterContract } from 'src/plugins/data/public'; import { LensFilterEvent } from './types'; /** replaces the value `(empty) to empty string for proper filtering` */ @@ -49,3 +50,32 @@ export function getVisualizeGeoFieldMessage(fieldType: string) { values: { fieldType }, }); } + +export const getResolvedDateRange = function (timefilter: TimefilterContract) { + const { from, to } = timefilter.getTime(); + const { min, max } = timefilter.calculateBounds({ + from, + to, + }); + return { fromDate: min?.toISOString() || from, toDate: max?.toISOString() || to }; +}; + +export function containsDynamicMath(dateMathString: string) { + return dateMathString.includes('now'); +} +export const TIME_LAG_PERCENTAGE_LIMIT = 0.02; + +export async function getAllIndexPatterns( + ids: string[], + indexPatternsService: IndexPatternsContract +): Promise<{ indexPatterns: IndexPattern[]; rejectedIds: string[] }> { + const responses = await Promise.allSettled(ids.map((id) => indexPatternsService.get(id))); + const fullfilled = responses.filter( + (response): response is PromiseFulfilledResult => response.status === 'fulfilled' + ); + const rejectedIds = responses + .map((_response, i) => ids[i]) + .filter((id, i) => responses[i].status === 'rejected'); + // return also the rejected ids in case we want to show something later on + return { indexPatterns: fullfilled.map((response) => response.value), rejectedIds }; +} diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index dda1a444f45448..19cfcb1a60cc7c 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import _ from 'lodash'; +import { uniq } from 'lodash'; import { render } from 'react-dom'; import { Position } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; @@ -43,7 +43,7 @@ function getVisualizationType(state: State): VisualizationType | 'mixed' { ); } const visualizationType = visualizationTypes.find((t) => t.id === state.layers[0].seriesType); - const seriesTypes = _.uniq(state.layers.map((l) => l.seriesType)); + const seriesTypes = uniq(state.layers.map((l) => l.seriesType)); return visualizationType && seriesTypes.length === 1 ? visualizationType : 'mixed'; } @@ -111,7 +111,7 @@ export const getXyVisualization = ({ }, appendLayer(state, layerId) { - const usedSeriesTypes = _.uniq(state.layers.map((layer) => layer.seriesType)); + const usedSeriesTypes = uniq(state.layers.map((layer) => layer.seriesType)); return { ...state, layers: [ @@ -255,10 +255,11 @@ export const getXyVisualization = ({ }, setDimension({ prevState, layerId, columnId, groupId }) { - const newLayer = prevState.layers.find((l) => l.layerId === layerId); - if (!newLayer) { + const foundLayer = prevState.layers.find((l) => l.layerId === layerId); + if (!foundLayer) { return prevState; } + const newLayer = { ...foundLayer }; if (groupId === 'x') { newLayer.xAccessor = columnId; @@ -277,11 +278,11 @@ export const getXyVisualization = ({ }, removeDimension({ prevState, layerId, columnId }) { - const newLayer = prevState.layers.find((l) => l.layerId === layerId); - if (!newLayer) { + const foundLayer = prevState.layers.find((l) => l.layerId === layerId); + if (!foundLayer) { return prevState; } - + const newLayer = { ...foundLayer }; if (newLayer.xAccessor === columnId) { delete newLayer.xAccessor; } else if (newLayer.splitAccessor === columnId) { diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index 3037513ccd56e8..a6517894654ede 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -140,7 +140,7 @@ const getDataBounds = function ( let min = Number.MAX_VALUE; let max = Number.MIN_VALUE; axis.series.forEach((series) => { - activeData?.[series.layer].rows.forEach((row) => { + activeData?.[series.layer]?.rows.forEach((row) => { const value = row[series.accessor]; if (!Number.isNaN(value)) { if (value < min) { diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts new file mode 100644 index 00000000000000..ddc822f37b95bf --- /dev/null +++ b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.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 { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; +import { SerializableState } from '../../../../../src/plugins/kibana_utils/common'; +import { DOC_TYPE } from '../../common'; +import { commonRenameOperationsForFormula } from '../migrations/common_migrations'; +import { LensDocShapePre712 } from '../migrations/types'; + +export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { + return { + id: DOC_TYPE, + migrations: { + // This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed. + '7.13.1': (state) => { + const lensState = (state as unknown) as { attributes: LensDocShapePre712 }; + const migratedLensState = commonRenameOperationsForFormula(lensState.attributes); + return ({ + ...lensState, + attributes: migratedLensState, + } as unknown) as SerializableState; + }, + }, + }; +}; diff --git a/x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap b/x-pack/plugins/lens/server/migrations/__snapshots__/saved_object_migrations.test.ts.snap similarity index 100% rename from x-pack/plugins/lens/server/__snapshots__/migrations.test.ts.snap rename to x-pack/plugins/lens/server/migrations/__snapshots__/saved_object_migrations.test.ts.snap diff --git a/x-pack/plugins/lens/server/migrations/common_migrations.ts b/x-pack/plugins/lens/server/migrations/common_migrations.ts new file mode 100644 index 00000000000000..85055e471bac95 --- /dev/null +++ b/x-pack/plugins/lens/server/migrations/common_migrations.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep } from 'lodash'; +import { LensDocShapePre712, OperationTypePre712, LensDocShapePost712 } from './types'; + +export const commonRenameOperationsForFormula = ( + attributes: LensDocShapePre712 +): LensDocShapePost712 => { + const renameMapping = { + avg: 'average', + cardinality: 'unique_count', + derivative: 'differences', + } as const; + function shouldBeRenamed(op: OperationTypePre712): op is keyof typeof renameMapping { + return op in renameMapping; + } + const newAttributes = cloneDeep(attributes); + const datasourceLayers = newAttributes.state.datasourceStates.indexpattern.layers || {}; + (newAttributes as LensDocShapePost712).state.datasourceStates.indexpattern.layers = Object.fromEntries( + Object.entries(datasourceLayers).map(([layerId, layer]) => { + return [ + layerId, + { + ...layer, + columns: Object.fromEntries( + Object.entries(layer.columns).map(([columnId, column]) => { + const copy = { + ...column, + operationType: shouldBeRenamed(column.operationType) + ? renameMapping[column.operationType] + : column.operationType, + }; + return [columnId, copy]; + }) + ), + }, + ]; + }) + ); + return newAttributes as LensDocShapePost712; +}; diff --git a/x-pack/plugins/lens/server/migrations.test.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts similarity index 98% rename from x-pack/plugins/lens/server/migrations.test.ts rename to x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts index bed19942e52bcd..5478d86e9b14c7 100644 --- a/x-pack/plugins/lens/server/migrations.test.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.test.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { migrations, LensDocShape } from './migrations'; -import { SavedObjectMigrationContext, SavedObjectMigrationFn } from 'src/core/server'; +import { migrations, LensDocShape } from './saved_object_migrations'; +import { + SavedObjectMigrationContext, + SavedObjectMigrationFn, + SavedObjectUnsanitizedDoc, +} from 'src/core/server'; describe('Lens migrations', () => { describe('7.7.0 missing dimensions in XY', () => { @@ -767,10 +771,7 @@ describe('Lens migrations', () => { }, }; - it('should rename only specific operation types', () => { - const result = migrations['7.13.0'](example, context) as ReturnType< - SavedObjectMigrationFn - >; + const validate = (result: SavedObjectUnsanitizedDoc>) => { const layers = result.attributes.state.datasourceStates.indexpattern.layers; expect(layers).toEqual({ '5ab74ddc-93ca-44e2-9857-ecf85c86b53e': { @@ -832,6 +833,23 @@ describe('Lens migrations', () => { expect(result.attributes.state.query).toEqual(example.attributes.state.query); expect(result.attributes.state.filters).toEqual(example.attributes.state.filters); expect(result.attributes.title).toEqual(example.attributes.title); + }; + + it('should rename only specific operation types', () => { + const result = migrations['7.13.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + validate(result); + }); + + it('can be applied multiple times', () => { + const result1 = migrations['7.13.0'](example, context) as ReturnType< + SavedObjectMigrationFn + >; + const result2 = migrations['7.13.1'](result1, context) as ReturnType< + SavedObjectMigrationFn + >; + validate(result2); }); }); }); diff --git a/x-pack/plugins/lens/server/migrations.ts b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts similarity index 80% rename from x-pack/plugins/lens/server/migrations.ts rename to x-pack/plugins/lens/server/migrations/saved_object_migrations.ts index 430c1a6caa6675..ba7004ba67a958 100644 --- a/x-pack/plugins/lens/server/migrations.ts +++ b/x-pack/plugins/lens/server/migrations/saved_object_migrations.ts @@ -14,7 +14,9 @@ import { SavedObjectUnsanitizedDoc, } from 'src/core/server'; import { Query, Filter } from 'src/plugins/data/public'; -import { PersistableFilter } from '../common'; +import { PersistableFilter } from '../../common'; +import { LensDocShapePost712, LensDocShapePre712 } from './types'; +import { commonRenameOperationsForFormula } from './common_migrations'; interface LensDocShapePre710 { visualizationType: string | null; @@ -106,86 +108,6 @@ interface DatatableStatePost711 { }; } -type OperationTypePre712 = - | 'avg' - | 'cardinality' - | 'derivative' - | 'filters' - | 'terms' - | 'date_histogram' - | 'min' - | 'max' - | 'sum' - | 'median' - | 'percentile' - | 'last_value' - | 'count' - | 'range' - | 'cumulative_sum' - | 'counter_rate' - | 'moving_average'; -type OperationTypePost712 = Exclude< - OperationTypePre712 | 'average' | 'unique_count' | 'differences', - 'avg' | 'cardinality' | 'derivative' ->; -interface LensDocShapePre712 { - visualizationType: string | null; - title: string; - expression: string | null; - state: { - datasourceStates: { - // This is hardcoded as our only datasource - indexpattern: { - layers: Record< - string, - { - columns: Record< - string, - { - operationType: OperationTypePre712; - } - >; - } - >; - }; - }; - visualization: VisualizationState; - query: Query; - filters: Filter[]; - }; -} - -interface LensDocShapePost712 { - visualizationType: string | null; - title: string; - expression: string | null; - state: { - datasourceMetaData: { - filterableIndexPatterns: Array<{ id: string; title: string }>; - }; - datasourceStates: { - // This is hardcoded as our only datasource - indexpattern: { - currentIndexPatternId: string; - layers: Record< - string, - { - columns: Record< - string, - { - operationType: OperationTypePost712; - } - >; - } - >; - }; - }; - visualization: VisualizationState; - query: Query; - filters: Filter[]; - }; -} - /** * Removes the `lens_auto_date` subexpression from a stored expression * string. For example: aggConfigs={lens_auto_date aggConfigs="JSON string"} @@ -471,38 +393,11 @@ const renameOperationsForFormula: SavedObjectMigrationFn< LensDocShapePre712, LensDocShapePost712 > = (doc) => { - const renameMapping = { - avg: 'average', - cardinality: 'unique_count', - derivative: 'differences', - } as const; - function shouldBeRenamed(op: OperationTypePre712): op is keyof typeof renameMapping { - return op in renameMapping; - } const newDoc = cloneDeep(doc); - const datasourceLayers = newDoc.attributes.state.datasourceStates.indexpattern.layers || {}; - (newDoc.attributes as LensDocShapePost712).state.datasourceStates.indexpattern.layers = Object.fromEntries( - Object.entries(datasourceLayers).map(([layerId, layer]) => { - return [ - layerId, - { - ...layer, - columns: Object.fromEntries( - Object.entries(layer.columns).map(([columnId, column]) => { - const copy = { - ...column, - operationType: shouldBeRenamed(column.operationType) - ? renameMapping[column.operationType] - : column.operationType, - }; - return [columnId, copy]; - }) - ), - }, - ]; - }) - ); - return newDoc as SavedObjectUnsanitizedDoc; + return { + ...newDoc, + attributes: commonRenameOperationsForFormula(newDoc.attributes), + }; }; export const migrations: SavedObjectMigrationMap = { @@ -514,4 +409,5 @@ export const migrations: SavedObjectMigrationMap = { '7.11.0': removeSuggestedPriority, '7.12.0': transformTableState, '7.13.0': renameOperationsForFormula, + '7.13.1': renameOperationsForFormula, // duplicate this migration in case a broken by value panel is added to the library }; diff --git a/x-pack/plugins/lens/server/migrations/types.ts b/x-pack/plugins/lens/server/migrations/types.ts new file mode 100644 index 00000000000000..38e079ff380516 --- /dev/null +++ b/x-pack/plugins/lens/server/migrations/types.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Query, Filter } from 'src/plugins/data/public'; + +export type OperationTypePre712 = + | 'avg' + | 'cardinality' + | 'derivative' + | 'filters' + | 'terms' + | 'date_histogram' + | 'min' + | 'max' + | 'sum' + | 'median' + | 'percentile' + | 'last_value' + | 'count' + | 'range' + | 'cumulative_sum' + | 'counter_rate' + | 'moving_average'; +export type OperationTypePost712 = Exclude< + OperationTypePre712 | 'average' | 'unique_count' | 'differences', + 'avg' | 'cardinality' | 'derivative' +>; + +export interface LensDocShapePre712 { + visualizationType: string | null; + title: string; + expression: string | null; + state: { + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + layers: Record< + string, + { + columns: Record< + string, + { + operationType: OperationTypePre712; + } + >; + } + >; + }; + }; + query: Query; + visualization: VisualizationState; + filters: Filter[]; + }; +} + +export interface LensDocShapePost712 { + visualizationType: string | null; + title: string; + expression: string | null; + state: { + datasourceMetaData: { + filterableIndexPatterns: Array<{ id: string; title: string }>; + }; + datasourceStates: { + // This is hardcoded as our only datasource + indexpattern: { + currentIndexPatternId: string; + layers: Record< + string, + { + columns: Record< + string, + { + operationType: OperationTypePost712; + } + >; + } + >; + }; + }; + visualization: VisualizationState; + query: Query; + filters: Filter[]; + }; +} diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index 92b14ba509bae9..c23c98cd12aec9 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -17,10 +17,13 @@ import { scheduleLensTelemetry, } from './usage'; import { setupSavedObjects } from './saved_objects'; +import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server'; +import { lensEmbeddableFactory } from './embeddable/lens_embeddable_factory'; export interface PluginSetupContract { usageCollection?: UsageCollectionSetup; taskManager?: TaskManagerSetupContract; + embeddable: EmbeddableSetup; } export interface PluginStartContract { @@ -53,6 +56,7 @@ export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { plugins.taskManager ); } + plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory()); return {}; } diff --git a/x-pack/plugins/lens/server/saved_objects.ts b/x-pack/plugins/lens/server/saved_objects.ts index 202439abf03761..0266378981fd61 100644 --- a/x-pack/plugins/lens/server/saved_objects.ts +++ b/x-pack/plugins/lens/server/saved_objects.ts @@ -7,7 +7,7 @@ import { CoreSetup } from 'kibana/server'; import { getEditPath } from '../common'; -import { migrations } from './migrations'; +import { migrations } from './migrations/saved_object_migrations'; export function setupSavedObjects(core: CoreSetup) { core.savedObjects.registerType({ diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 0d8930bdb75b84..29eccdea4e8a6b 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -41,6 +41,7 @@ export const GIS_API_PATH = `api/${APP_ID}`; export const INDEX_SETTINGS_API_PATH = `${GIS_API_PATH}/indexSettings`; export const FONTS_API_PATH = `${GIS_API_PATH}/fonts`; export const INDEX_SOURCE_API_PATH = `${GIS_API_PATH}/docSource`; +export const INDEX_FEATURE_PATH = `/${GIS_API_PATH}/feature`; export const API_ROOT_PATH = `/${GIS_API_PATH}`; export const MVT_GETTILE_API_PATH = 'mvt/getTile'; diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index e1f682678df4b9..e65e19d7def482 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -13,6 +13,11 @@ import { RENDER_AS, SCALING_TYPES } from '../constants'; import { MapExtent, MapQuery } from './map_descriptor'; import { Filter, TimeRange } from '../../../../../src/plugins/data/common'; +export type Timeslice = { + from: number; + to: number; +}; + // Global map state passed to every layer. export type MapFilters = { buffer?: MapExtent; // extent with additional buffer @@ -22,6 +27,7 @@ export type MapFilters = { refreshTimerLastTriggeredAt?: string; searchSessionId?: string; timeFilters: TimeRange; + timeslice?: Timeslice; zoom: number; }; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js index a0908035c1480a..c2ca952c3e8c93 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.test.js @@ -396,7 +396,7 @@ describe('createExtentFilter', () => { minLat: 35, minLon: -89, }; - const filter = createExtentFilter(mapExtent, geoFieldName); + const filter = createExtentFilter(mapExtent, [geoFieldName]); expect(filter.geo_bounding_box).toEqual({ location: { top_left: [-89, 39], @@ -412,7 +412,7 @@ describe('createExtentFilter', () => { minLat: -100, minLon: -190, }; - const filter = createExtentFilter(mapExtent, geoFieldName); + const filter = createExtentFilter(mapExtent, [geoFieldName]); expect(filter.geo_bounding_box).toEqual({ location: { top_left: [-180, 89], @@ -428,7 +428,7 @@ describe('createExtentFilter', () => { minLat: 35, minLon: 100, }; - const filter = createExtentFilter(mapExtent, geoFieldName); + const filter = createExtentFilter(mapExtent, [geoFieldName]); const leftLon = filter.geo_bounding_box.location.top_left[0]; const rightLon = filter.geo_bounding_box.location.bottom_right[0]; expect(leftLon).toBeGreaterThan(rightLon); @@ -447,7 +447,7 @@ describe('createExtentFilter', () => { minLat: 35, minLon: -200, }; - const filter = createExtentFilter(mapExtent, geoFieldName); + const filter = createExtentFilter(mapExtent, [geoFieldName]); const leftLon = filter.geo_bounding_box.location.top_left[0]; const rightLon = filter.geo_bounding_box.location.bottom_right[0]; expect(leftLon).toBeGreaterThan(rightLon); @@ -466,7 +466,7 @@ describe('createExtentFilter', () => { minLat: 35, minLon: -191, }; - const filter = createExtentFilter(mapExtent, geoFieldName); + const filter = createExtentFilter(mapExtent, [geoFieldName]); expect(filter.geo_bounding_box).toEqual({ location: { top_left: [-180, 39], diff --git a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts index c18a79fa9dcbc3..e47afce77f7793 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/elasticsearch_geo_utils.ts @@ -349,18 +349,49 @@ export function makeESBbox({ maxLat, maxLon, minLat, minLon }: MapExtent): ESBBo return esBbox; } -export function createExtentFilter(mapExtent: MapExtent, geoFieldName: string): GeoFilter { - return { - geo_bounding_box: { - [geoFieldName]: makeESBbox(mapExtent), - }, - meta: { - alias: null, - disabled: false, - negate: false, - key: geoFieldName, - }, - }; +export function createExtentFilter(mapExtent: MapExtent, geoFieldNames: string[]): GeoFilter { + const esBbox = makeESBbox(mapExtent); + return geoFieldNames.length === 1 + ? { + geo_bounding_box: { + [geoFieldNames[0]]: esBbox, + }, + meta: { + alias: null, + disabled: false, + negate: false, + key: geoFieldNames[0], + }, + } + : { + query: { + bool: { + should: geoFieldNames.map((geoFieldName) => { + return { + bool: { + must: [ + { + exists: { + field: geoFieldName, + }, + }, + { + geo_bounding_box: { + [geoFieldName]: esBbox, + }, + }, + ], + }, + }; + }), + }, + }, + meta: { + alias: null, + disabled: false, + negate: false, + }, + }; } export function createSpatialFilterWithGeometry({ diff --git a/x-pack/plugins/maps/common/types.ts b/x-pack/plugins/maps/common/types.ts index 6f2bd72c808967..6ca3de3dac377c 100644 --- a/x-pack/plugins/maps/common/types.ts +++ b/x-pack/plugins/maps/common/types.ts @@ -6,6 +6,7 @@ */ export interface CreateDocSourceResp { + indexPatternId?: string; success: boolean; error?: Error; } diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.js index fafafa6b6a0711..77ce23594447f8 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.js @@ -271,6 +271,9 @@ describe('map_actions', () => { require('../selectors/map_selectors').getTimeFilters = () => { return timeFilters; }; + require('../selectors/map_selectors').getTimeslice = () => { + return undefined; + }; require('../selectors/map_selectors').getFilters = () => { return filters; }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 9682306852ba92..32e17376acaa75 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -10,7 +10,6 @@ import { AnyAction, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; - import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { @@ -20,6 +19,7 @@ import { getWaitingForMapReadyLayerListRaw, getQuery, getTimeFilters, + getTimeslice, getLayerList, getSearchSessionId, getSearchSessionMapBuffer, @@ -54,6 +54,7 @@ import { MapCenterAndZoom, MapExtent, MapRefreshConfig, + Timeslice, } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; import { scaleBounds } from '../../common/elasticsearch_util'; @@ -227,17 +228,21 @@ function generateQueryTimestamp() { export function setQuery({ query, timeFilters, - filters = [], + timeslice, + filters, forceRefresh = false, searchSessionId, searchSessionMapBuffer, + clearTimeslice, }: { filters?: Filter[]; query?: Query; timeFilters?: TimeRange; + timeslice?: Timeslice; forceRefresh?: boolean; searchSessionId?: string; searchSessionMapBuffer?: MapExtent; + clearTimeslice?: boolean; }) { return async ( dispatch: ThunkDispatch, @@ -249,10 +254,24 @@ export function setQuery({ ? prevQuery.queryLastTriggeredAt : generateQueryTimestamp(); + const prevTimeFilters = getTimeFilters(getState()); + + function getNextTimeslice() { + if ( + clearTimeslice || + (timeFilters !== undefined && !_.isEqual(timeFilters, prevTimeFilters)) + ) { + return undefined; + } + + return timeslice ? timeslice : getTimeslice(getState()); + } + const nextQueryContext = { - timeFilters: timeFilters ? timeFilters : getTimeFilters(getState()), + timeFilters: timeFilters ? timeFilters : prevTimeFilters, + timeslice: getNextTimeslice(), query: { - ...(query ? query : getQuery(getState())), + ...(query ? query : prevQuery), // ensure query changes to trigger re-fetch when "Refresh" clicked queryLastTriggeredAt: forceRefresh ? generateQueryTimestamp() : prevTriggeredAt, }, @@ -262,8 +281,9 @@ export function setQuery({ }; const prevQueryContext = { - timeFilters: getTimeFilters(getState()), - query: getQuery(getState()), + timeFilters: prevTimeFilters, + timeslice: getTimeslice(getState()), + query: prevQuery, filters: getFilters(getState()), searchSessionId: getSearchSessionId(getState()), searchSessionMapBuffer: getSearchSessionMapBuffer(getState()), diff --git a/x-pack/plugins/maps/public/actions/ui_actions.ts b/x-pack/plugins/maps/public/actions/ui_actions.ts index f9c0e324aa5d8b..7809c71d7f6969 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.ts @@ -10,11 +10,12 @@ import { ThunkDispatch } from 'redux-thunk'; import { MapStoreState } from '../reducers/store'; import { getFlyoutDisplay } from '../selectors/ui_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; -import { trackMapSettings } from './map_actions'; +import { setQuery, trackMapSettings } from './map_actions'; import { setSelectedLayer } from './layer_actions'; export const UPDATE_FLYOUT = 'UPDATE_FLYOUT'; export const SET_IS_LAYER_TOC_OPEN = 'SET_IS_LAYER_TOC_OPEN'; +export const SET_IS_TIME_SLIDER_OPEN = 'SET_IS_TIME_SLIDER_OPEN'; export const SET_FULL_SCREEN = 'SET_FULL_SCREEN'; export const SET_READ_ONLY = 'SET_READ_ONLY'; export const SET_OPEN_TOC_DETAILS = 'SET_OPEN_TOC_DETAILS'; @@ -87,3 +88,20 @@ export function hideTOCDetails(layerId: string) { layerId, }; } + +export function openTimeslider() { + return { + type: SET_IS_TIME_SLIDER_OPEN, + isTimesliderOpen: true, + }; +} + +export function closeTimeslider() { + return (dispatch: ThunkDispatch) => { + dispatch({ + type: SET_IS_TIME_SLIDER_OPEN, + isTimesliderOpen: false, + }); + dispatch(setQuery({ clearTimeslice: true })); + }; +} diff --git a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx index 7d6f6757bef18f..024c2308df6c67 100644 --- a/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/file_upload_wizard/wizard.tsx @@ -11,13 +11,13 @@ import React, { Component } from 'react'; import { FeatureCollection } from 'geojson'; import { EuiPanel } from '@elastic/eui'; import { DEFAULT_MAX_RESULT_WINDOW, SCALING_TYPES } from '../../../../common/constants'; -import { getFileUpload } from '../../../kibana_services'; import { GeoJsonFileSource } from '../../sources/geojson_file_source'; import { VectorLayer } from '../../layers/vector_layer'; import { createDefaultLayerDescriptor } from '../../sources/es_search_source'; import { RenderWizardArguments } from '../../layers/layer_wizard_registry'; -import { FileUploadComponentProps, FileUploadGeoResults } from '../../../../../file_upload/public'; +import { FileUploadGeoResults } from '../../../../../file_upload/public'; import { ES_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { getFileUploadComponent } from '../../../kibana_services'; export enum UPLOAD_STEPS { CONFIGURE_UPLOAD = 'CONFIGURE_UPLOAD', @@ -34,7 +34,6 @@ enum INDEXING_STAGE { interface State { indexingStage: INDEXING_STAGE; - fileUploadComponent: React.ComponentType | null; results?: FileUploadGeoResults; } @@ -43,12 +42,10 @@ export class ClientFileCreateSourceEditor extends Component { if (!this._isMounted) { return; @@ -157,11 +147,8 @@ export class ClientFileCreateSourceEditor extends Component ( + + + + + + + + + + + +); diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index 26ebe53d9e3859..be113ab4cc2c9a 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { Query } from 'src/plugins/data/public'; import _ from 'lodash'; import React, { ReactElement, ReactNode } from 'react'; @@ -42,6 +42,7 @@ import { DataRequestContext } from '../../actions'; import { IStyle } from '../styles/style'; import { getJoinAggKey } from '../../../common/get_agg_key'; import { LICENSED_FEATURES } from '../../licensed_features'; +import { IESSource } from '../sources/es_source'; export interface ILayer { getBounds(dataRequestContext: DataRequestContext): Promise; @@ -101,6 +102,7 @@ export interface ILayer { getLicensedFeatures(): Promise; getCustomIconAndTooltipContent(): CustomIconAndTooltipContent; getDescriptor(): LayerDescriptor; + getGeoFieldNames(): string[]; } export type CustomIconAndTooltipContent = { @@ -513,4 +515,9 @@ export class AbstractLayer implements ILayer { async getLicensedFeatures(): Promise { return []; } + + getGeoFieldNames(): string[] { + const source = this.getSource(); + return source.isESSource() ? [(source as IESSource).getGeoFieldName()] : []; + } } diff --git a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts index 804352f5bede72..6ee863cfdb600c 100644 --- a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts +++ b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts @@ -30,6 +30,8 @@ import { mvtVectorSourceWizardConfig } from '../sources/mvt_single_layer_vector_ import { ObservabilityLayerWizardConfig } from './solution_layers/observability'; import { SecurityLayerWizardConfig } from './solution_layers/security'; import { choroplethLayerWizardConfig } from './choropleth_layer_wizard'; +import { newVectorLayerWizardConfig } from './new_vector_layer_wizard'; +import { getMapAppConfig } from '../../kibana_services'; let registered = false; export function registerLayerWizards() { @@ -39,6 +41,9 @@ export function registerLayerWizards() { // Registration order determines display order registerLayerWizard(uploadLayerWizardConfig); + if (getMapAppConfig().enableDrawingFeature) { + registerLayerWizard(newVectorLayerWizardConfig); + } registerLayerWizard(esDocumentsLayerWizardConfig); // @ts-ignore registerLayerWizard(choroplethLayerWizardConfig); diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/config.tsx b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/config.tsx new file mode 100644 index 00000000000000..2a0400c3d6beea --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/config.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; +import { NewVectorLayerEditor } from './wizard'; +import { DrawLayerIcon } from '../../layers/icons/draw_layer_icon'; +import { getFileUpload } from '../../../kibana_services'; +import { LAYER_WIZARD_CATEGORY } from '../../../../common'; + +const ADD_VECTOR_DRAWING_LAYER = 'ADD_VECTOR_DRAWING_LAYER'; + +export const newVectorLayerWizardConfig: LayerWizard = { + categories: [LAYER_WIZARD_CATEGORY.ELASTICSEARCH], + description: i18n.translate('xpack.maps.newVectorLayerWizard.description', { + defaultMessage: 'Creates a new empty layer. Use this to add shapes to the map', + }), + disabledReason: i18n.translate('xpack.maps.newVectorLayerWizard.disabledDesc', { + defaultMessage: + 'Unable to draw vector shapes, you are missing the Kibana privilege "Index Pattern Management".', + }), + getIsDisabled: async () => { + const hasImportPermission = await getFileUpload().hasImportPermission({ + checkCreateIndexPattern: true, + checkHasManagePipeline: false, + }); + return !hasImportPermission; + }, + icon: DrawLayerIcon, + prerequisiteSteps: [ + { + id: ADD_VECTOR_DRAWING_LAYER, + label: i18n.translate('xpack.maps.newVectorLayerWizard.indexNewLayer', { + defaultMessage: 'Index new layer', + }), + }, + ], + renderWizard: (renderWizardArguments: RenderWizardArguments) => { + return ; + }, + title: i18n.translate('xpack.maps.newVectorLayerWizard.title', { + defaultMessage: 'Create new layer', + }), +}; diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/create_new_index_pattern.ts b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/create_new_index_pattern.ts new file mode 100644 index 00000000000000..d612c25157095f --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/create_new_index_pattern.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 { getHttp } from '../../../kibana_services'; +import { CreateDocSourceResp, INDEX_SOURCE_API_PATH } from '../../../../common'; + +export const createNewIndexAndPattern = async (indexName: string) => { + return await getHttp().fetch({ + path: `/${INDEX_SOURCE_API_PATH}`, + method: 'POST', + body: JSON.stringify({ + index: indexName, + // Initially set to static mappings + mappings: { + properties: { + coordinates: { + type: 'geo_shape', + }, + }, + }, + }), + }); +}; diff --git a/x-pack/plugins/beats_management/server/routes/tokens/index.ts b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/index.ts similarity index 82% rename from x-pack/plugins/beats_management/server/routes/tokens/index.ts rename to x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/index.ts index f04a8e0d5bc5a0..c183c19f6e2af9 100644 --- a/x-pack/plugins/beats_management/server/routes/tokens/index.ts +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { registerCreateTokenRoute } from './create'; +export { newVectorLayerWizardConfig } from './config'; diff --git a/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx new file mode 100644 index 00000000000000..0d39c1c720bf22 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/new_vector_layer_wizard/wizard.tsx @@ -0,0 +1,161 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Component, Fragment } from 'react'; +import { EuiEmptyPrompt, EuiPanel, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { createNewIndexAndPattern } from './create_new_index_pattern'; +import { RenderWizardArguments } from '../layer_wizard_registry'; +import { VectorLayer } from '../vector_layer'; +import { ESSearchSource } from '../../sources/es_search_source'; +import { ADD_LAYER_STEP_ID } from '../../../connected_components/add_layer_panel/view'; +import { getIndexNameFormComponent } from '../../../kibana_services'; + +interface State { + indexName: string; + indexNameError: string; + indexingTriggered: boolean; + createIndexError: string; +} + +export class NewVectorLayerEditor extends Component { + private _isMounted: boolean = false; + + state: State = { + indexName: '', + indexNameError: '', + indexingTriggered: false, + createIndexError: '', + }; + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + + async componentDidUpdate() { + if (this.props.currentStepId === ADD_LAYER_STEP_ID && !this.state.indexingTriggered) { + this.setState({ indexingTriggered: true }); + await this._createNewIndex(); + } + } + + _setCreateIndexError(errorMessage: string) { + if (!this._isMounted) { + return; + } + this.setState({ + createIndexError: errorMessage, + }); + } + + _createNewIndex = async () => { + let indexPatternId: string | undefined; + try { + const response = await createNewIndexAndPattern(this.state.indexName); + indexPatternId = response.indexPatternId; + } catch (e) { + this._setCreateIndexError(e.message); + return; + } + + if (!indexPatternId) { + this._setCreateIndexError( + i18n.translate('xpack.maps.layers.newVectorLayerWizard.createIndexError', { + defaultMessage: 'Could not create index with name {message}', + values: { + message: this.state.indexName, + }, + }) + ); + return; + } + + if (!this._isMounted) { + return; + } + // Creates empty layer + const sourceDescriptor = ESSearchSource.createDescriptor({ + indexPatternId, + geoField: 'coordinates', + filterByMapBounds: false, + }); + const layerDescriptor = VectorLayer.createDescriptor( + { sourceDescriptor }, + this.props.mapColors + ); + this.props.previewLayers([layerDescriptor]); + this.props.advanceToNextStep(); + }; + + _onIndexChange = (indexName: string, indexError?: string) => { + this.setState({ + indexName, + indexNameError: indexError ? indexError : '', + }); + if (indexName && !indexError) { + this.props.enableNextBtn(); + } else { + this.props.disableNextBtn(); + } + }; + + render() { + if (this.state.createIndexError) { + return ( + +

{this.state.createIndexError}

+
+ ); + } + + const IndexNameForm = getIndexNameFormComponent(); + return ( + + <> + + {i18n.translate('xpack.maps.layers.newVectorLayerWizard.createNewLayer', { + defaultMessage: 'Create new layer', + })} + + } + body={ + +

+ {i18n.translate( + 'xpack.maps.layers.newVectorLayerWizard.vectorEditorDescription', + { + defaultMessage: `Creates a new vector layer. This can be used to draw and store new shapes.`, + } + )} +

+
+ } + /> + {}} + onIndexNameValidationEnd={() => {}} + /> + +
+ ); + } +} diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index d452096250576d..6dba935ccc87d9 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { +import type { Map as MbMap, GeoJSONSource as MbGeoJSONSource, VectorSource as MbVectorSource, -} from 'mapbox-gl'; +} from '@kbn/mapbox-gl'; import { EuiIcon } from '@elastic/eui'; import { Feature } from 'geojson'; import uuid from 'uuid/v4'; diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx index e49339b6250b48..5f42d80de9bbbd 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx @@ -6,7 +6,7 @@ */ import { FeatureCollection } from 'geojson'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { EMPTY_FEATURE_COLLECTION, SOURCE_BOUNDS_DATA_REQUEST_ID, @@ -136,6 +136,7 @@ export async function getVectorSourceBounds({ sourceQuery: sourceQuery ? sourceQuery : undefined, query: dataFilters.query, timeFilters: dataFilters.timeFilters, + timeslice: dataFilters.timeslice, filters: dataFilters.filters, applyGlobalQuery: source.getApplyGlobalQuery(), applyGlobalTime: source.getApplyGlobalTime(), 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 a1474237f6c9cc..ca171f10207e16 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 @@ -6,7 +6,11 @@ */ import React from 'react'; -import { Map as MbMap, Layer as MbLayer, GeoJSONSource as MbGeoJSONSource } from 'mapbox-gl'; +import type { + Map as MbMap, + AnyLayer as MbLayer, + GeoJSONSource as MbGeoJSONSource, +} from '@kbn/mapbox-gl'; import { Feature, FeatureCollection, GeoJsonProperties } from 'geojson'; import _ from 'lodash'; import { EuiIcon } from '@elastic/eui'; @@ -89,6 +93,7 @@ export interface IVectorLayer extends ILayer { getPropertiesForTooltip(properties: GeoJsonProperties): Promise; hasJoins(): boolean; canShowTooltip(): boolean; + getLeftJoinFields(): Promise; } export class VectorLayer extends AbstractLayer implements IVectorLayer { diff --git a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx index abac9cbe5d0267..265a0f67f959c2 100644 --- a/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx @@ -12,7 +12,7 @@ import { TooltipSelector } from '../../../components/tooltip_selector'; import { getEmsFileLayers } from '../../../util'; import { IEmsFileSource } from './ems_file_source'; import { IField } from '../../fields/field'; -import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; +import { OnSourceChangeArgs } from '../source'; interface Props { layerId: string; diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx index 81c5d8d6d89df4..2688ef1d29686f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx @@ -18,7 +18,7 @@ import { MetricsEditor } from '../../../components/metrics_editor'; import { getIndexPatternService } from '../../../kibana_services'; import { GeoLineForm } from './geo_line_form'; import { AggDescriptor } from '../../../../common/descriptor_types'; -import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; +import { OnSourceChangeArgs } from '../source'; interface Props { indexPatternId: string; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx index b9ce43dbbdad42..5d095035331f8c 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx @@ -22,7 +22,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { getIndexPatternService } from '../../../kibana_services'; import { DEFAULT_MAX_RESULT_WINDOW, LAYER_TYPE, SCALING_TYPES } from '../../../../common/constants'; import { loadIndexSettings } from './load_index_settings'; -import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; +import { OnSourceChangeArgs } from '../source'; interface Props { filterByMapBounds: boolean; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx index ec656be3efeaea..41c9076c114360 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx @@ -19,7 +19,7 @@ import { SortDirection, } from '../../../../../../../../src/plugins/data/common'; import { TopHitsForm } from './top_hits_form'; -import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view'; +import { OnSourceChangeArgs } from '../../source'; interface Props { onSourceConfigChange: (sourceConfig: Partial | null) => void; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx index e4f196e5e8a858..83831f84e77f98 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx @@ -14,7 +14,7 @@ import { getIndexPatternService } from '../../../../kibana_services'; import { ValidatedRange } from '../../../../components/validated_range'; import { DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../../../../common/constants'; import { loadIndexSettings } from '../load_index_settings'; -import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view'; +import { OnSourceChangeArgs } from '../../source'; import { IFieldType, SortDirection } from '../../../../../../../../src/plugins/data/public'; interface Props { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx index 90553d47e644a5..6ca38f3be22f48 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx @@ -16,7 +16,7 @@ import { getIndexPatternService } from '../../../../kibana_services'; import { getTermsFields, getSortFields, getSourceFields } from '../../../../index_pattern_util'; import { SortDirection, IFieldType } from '../../../../../../../../src/plugins/data/public'; import { ESDocField } from '../../../fields/es_doc_field'; -import { OnSourceChangeArgs } from '../../../../connected_components/layer_panel/view'; +import { OnSourceChangeArgs } from '../../source'; import { TopHitsForm } from './top_hits_form'; import { ESSearchSource } from '../es_search_source'; import { IField } from '../../../fields/field'; diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 8e31ad78551970..23bcd9baed8c0b 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -213,12 +213,19 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource typeof searchFilters.geogridPrecision === 'number' ? expandToTileBoundaries(searchFilters.buffer, searchFilters.geogridPrecision) : searchFilters.buffer; - const extentFilter = createExtentFilter(buffer, geoField.name); + const extentFilter = createExtentFilter(buffer, [geoField.name]); allFilters.push(extentFilter); } if (searchFilters.applyGlobalTime && (await this.isTimeAware())) { - const filter = getTimeFilter().createFilter(indexPattern, searchFilters.timeFilters); + const timeRange = searchFilters.timeslice + ? { + from: new Date(searchFilters.timeslice.from).toISOString(), + to: new Date(searchFilters.timeslice.to).toISOString(), + mode: 'absolute' as 'absolute', + } + : searchFilters.timeFilters; + const filter = getTimeFilter().createFilter(indexPattern, timeRange); if (filter) { allFilters.push(filter); } diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx index a20aadd3c12bc0..62ebdb789daebc 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/update_source_editor.tsx @@ -14,7 +14,7 @@ import { TooltipSelector } from '../../../components/tooltip_selector'; import { MVTField } from '../../fields/mvt_field'; import { MVTSingleLayerVectorSource } from './mvt_single_layer_vector_source'; import { MVTSettings, MVTSingleLayerSourceSettings } from './mvt_single_layer_source_settings'; -import { OnSourceChangeArgs } from '../../../connected_components/layer_panel/view'; +import { OnSourceChangeArgs } from '../source'; import { MVTFieldDescriptor } from '../../../../common/descriptor_types'; interface Props { diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 28a68dda64cb6b..1f683068e132bf 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -14,12 +14,17 @@ import { GeoJsonProperties } from 'geojson'; import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { IField } from '../fields/field'; -import { FieldFormatter, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; +import { FieldFormatter, LAYER_TYPE, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types'; -import { OnSourceChangeArgs } from '../../connected_components/layer_panel/view'; import { LICENSED_FEATURES } from '../../licensed_features'; import { PreIndexedShape } from '../../../common/elasticsearch_util'; +export type OnSourceChangeArgs = { + propName: string; + value: unknown; + newLayerType?: LAYER_TYPE; +}; + export type SourceEditorArgs = { onChange: (...args: OnSourceChangeArgs[]) => void; currentLayerType?: string; diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index da5a236a20936c..eabc5c4314d628 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -15,6 +15,7 @@ import { ESSearchSourceResponseMeta, MapExtent, MapQuery, + Timeslice, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; @@ -39,6 +40,7 @@ export interface BoundsFilters { query?: MapQuery; sourceQuery?: MapQuery; timeFilters: TimeRange; + timeslice?: Timeslice; } export interface IVectorSource extends ISource { diff --git a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx index fe581a1807b28b..723390ff236762 100644 --- a/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { i18n } from '@kbn/i18n'; import { EuiIcon } from '@elastic/eui'; import { IStyle } from '../style'; 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 d654cdc6bff514..73f8736750656e 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 @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import React from 'react'; import { EuiTextColor } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx index ad87b43c8e4a51..56d7c8597e1511 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { EuiTextColor } from '@elastic/eui'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; // @ts-expect-error import { getIconPalette, getMakiIconId, getMakiSymbolAnchor } from '../symbol_utils'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts index e72b411909e825..afa034b4d395c1 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty, getNumericalMbFeatureStateValue } from './dynamic_style_property'; import { OrientationDynamicOptions } from '../../../../../common/descriptor_types'; import { RawValue } from '../../../../../common/constants'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx index 64a3e0cf0e322d..e1a92fdcad08a8 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.test.tsx @@ -18,7 +18,7 @@ import { shallow } from 'enzyme'; import { DynamicSizeProperty } from './dynamic_size_property'; import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; import { IField } from '../../../fields/field'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { SizeDynamicOptions } from '../../../../../common/descriptor_types'; import { mockField, MockLayer, MockStyle } from './test_helpers/test_util'; import { IVectorLayer } from '../../../layers/vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx index 7076775dcce31f..8599adef33f011 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.tsx @@ -7,7 +7,7 @@ import _ from 'lodash'; import React from 'react'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; import { OrdinalLegend } from '../components/legend/ordinal_legend'; import { makeMbClampedNumberExpression } from '../style_util'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx index 9ffd9a0f1b3459..0841bb7546d9e7 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.tsx @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { Feature, FeatureCollection } from 'geojson'; -import { FeatureIdentifier, Map as MbMap } from 'mapbox-gl'; +import type { FeatureIdentifier, Map as MbMap } from '@kbn/mapbox-gl'; import { AbstractStyleProperty, IStyleProperty } from './style_property'; import { DEFAULT_SIGMA } from '../vector_style_defaults'; import { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx index 4550a27ac2d9a5..b5872773fdce63 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { DynamicTextProperty } from './dynamic_text_property'; import { RawValue, VECTOR_STYLES } from '../../../../../common/constants'; import { IField } from '../../../fields/field'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { mockField, MockLayer, MockStyle } from './test_helpers/test_util'; import { IVectorLayer } from '../../../layers/vector_layer'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts index e8612388a5ae16..c59d7207983718 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { DynamicStyleProperty } from './dynamic_style_property'; import { LabelDynamicOptions } from '../../../../../common/descriptor_types'; import { RawValue } from '../../../../../common/constants'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/label_border_size_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/label_border_size_property.ts index 4d9473a9adced9..5d4425a9d4995a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/label_border_size_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/label_border_size_property.ts @@ -6,7 +6,7 @@ */ import _ from 'lodash'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { AbstractStyleProperty } from './style_property'; import { DEFAULT_LABEL_SIZE } from '../vector_style_defaults'; import { LABEL_BORDER_SIZES } from '../../../../../common/constants'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_color_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_color_property.ts index dae983c4ae4fe0..c8a80b5ee28dc6 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_color_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_color_property.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; import { ColorStaticOptions } from '../../../../../common/descriptor_types'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts index 887f16a1760482..3a61e1a0748680 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; // @ts-expect-error import { getMakiSymbolAnchor, getMakiIconId } from '../symbol_utils'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_orientation_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_orientation_property.ts index 329088fdb160d7..48a52f641dfc72 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_orientation_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_orientation_property.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; import { VECTOR_STYLES } from '../../../../../common/constants'; import { OrientationStaticOptions } from '../../../../../common/descriptor_types'; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts index 3dd706043d1584..de71d07aa71673 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; import { VECTOR_STYLES } from '../../../../../common/constants'; import { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.test.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.test.ts index 70c7721e760b64..498ad2fc4ea176 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.test.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.test.ts @@ -7,7 +7,7 @@ import { StaticTextProperty } from './static_text_property'; import { VECTOR_STYLES } from '../../../../../common/constants'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; export class MockMbMap { _paintPropertyCalls: unknown[]; diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts index fb05fa052db21e..083d091aaefe54 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { StaticStyleProperty } from './static_style_property'; import { LabelStaticOptions } from '../../../../../common/descriptor_types'; 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 692be08d07bc6a..7578695d7ac6fa 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 @@ -7,7 +7,7 @@ import _ from 'lodash'; import React, { ReactElement } from 'react'; -import { FeatureIdentifier, Map as MbMap } from 'mapbox-gl'; +import { FeatureIdentifier, Map as MbMap } from '@kbn/mapbox-gl'; import { FeatureCollection } from 'geojson'; import { StyleProperties, VectorStyleEditor } from './components/vector_style_editor'; import { getDefaultStaticProperties, LINE_STYLES, POLYGON_STYLES } from './vector_style_defaults'; diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js index 1901b15e8f3506..c13b2fd441cad9 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js @@ -287,4 +287,322 @@ describe('canSkipSourceUpdate', () => { }); }); }); + + describe('isTimeAware', () => { + function createSourceMock() { + return { + isTimeAware: () => { + return true; + }, + isRefreshTimerAware: () => { + return false; + }, + isFilterByMapBounds: () => { + return false; + }, + isFieldAware: () => { + return false; + }, + isQueryAware: () => { + return false; + }, + isGeoGridPrecisionAware: () => { + return false; + }, + }; + } + + describe('applyGlobalTime', () => { + it('can not skip update when applyGlobalTime changes', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: createSourceMock(), + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: false, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can skip update when applyGlobalTime does not change', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: createSourceMock(), + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: true, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(true); + }); + }); + + describe('timeFilters', () => { + it('can not skip update when time range changes', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: createSourceMock(), + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-15m', + to: 'now', + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can skip update when time range does not change', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: createSourceMock(), + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-15m', + to: 'now', + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-15m', + to: 'now', + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can skip update when time range changes but applyGlobalTime is false', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: createSourceMock(), + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: false, + timeFilters: { + from: 'now-15m', + to: 'now', + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: false, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(true); + }); + }); + + describe('timeslice', () => { + const mockSource = createSourceMock(); + it('can not skip update when timeslice changes (undefined => provided)', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: mockSource, + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 0, + to: 1000, + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when timeslice changes', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: mockSource, + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 0, + to: 1000, + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 1000, + to: 2000, + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can not skip update when timeslice changes (provided => undefined)', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: mockSource, + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 0, + to: 1000, + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(false); + }); + + it('can skip update when timeslice does not change', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: mockSource, + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 0, + to: 1000, + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: true, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 0, + to: 1000, + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(true); + }); + + it('can skip update when timeslice changes but applyGlobalTime is false', async () => { + const canSkipUpdate = await canSkipSourceUpdate({ + source: mockSource, + prevDataRequest: new DataRequest({ + dataId: SOURCE_DATA_REQUEST_ID, + dataMeta: { + applyGlobalTime: false, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 0, + to: 1000, + }, + }, + data: {}, + }), + nextMeta: { + applyGlobalTime: false, + timeFilters: { + from: 'now-7d', + to: 'now', + }, + timeslice: { + from: 1000, + to: 2000, + }, + }, + extentAware: false, + }); + + expect(canSkipUpdate).toBe(true); + }); + }); + }); }); diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 575c99432f508c..1f2678f40eecd3 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -89,10 +89,12 @@ export async function canSkipSourceUpdate({ let updateDueToApplyGlobalTime = false; let updateDueToTime = false; + let updateDueToTimeslice = false; if (timeAware) { updateDueToApplyGlobalTime = prevMeta.applyGlobalTime !== nextMeta.applyGlobalTime; if (nextMeta.applyGlobalTime) { updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); + updateDueToTimeslice = !_.isEqual(prevMeta.timeslice, nextMeta.timeslice); } } @@ -148,6 +150,7 @@ export async function canSkipSourceUpdate({ return ( !updateDueToApplyGlobalTime && !updateDueToTime && + !updateDueToTimeslice && !updateDueToRefreshTimer && !updateDueToExtentChange && !updateDueToFields && diff --git a/x-pack/plugins/maps/public/components/global_filter_checkbox.js b/x-pack/plugins/maps/public/components/global_filter_checkbox.tsx similarity index 70% rename from x-pack/plugins/maps/public/components/global_filter_checkbox.js rename to x-pack/plugins/maps/public/components/global_filter_checkbox.tsx index df31cc5c40cdf4..bddb1cfd9cfcd5 100644 --- a/x-pack/plugins/maps/public/components/global_filter_checkbox.js +++ b/x-pack/plugins/maps/public/components/global_filter_checkbox.tsx @@ -6,10 +6,16 @@ */ import React from 'react'; -import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; -export function GlobalFilterCheckbox({ applyGlobalQuery, label, setApplyGlobalQuery }) { - const onApplyGlobalQueryChange = (event) => { +interface Props { + applyGlobalQuery: boolean; + label: string; + setApplyGlobalQuery: (applyGlobalQuery: boolean) => void; +} + +export function GlobalFilterCheckbox({ applyGlobalQuery, label, setApplyGlobalQuery }: Props) { + const onApplyGlobalQueryChange = (event: EuiSwitchEvent) => { setApplyGlobalQuery(event.target.checked); }; diff --git a/x-pack/plugins/maps/public/connected_components/_index.scss b/x-pack/plugins/maps/public/connected_components/_index.scss index c4438a9312bceb..56fcdc91659743 100644 --- a/x-pack/plugins/maps/public/connected_components/_index.scss +++ b/x-pack/plugins/maps/public/connected_components/_index.scss @@ -1,5 +1,6 @@ @import 'map_container/map_container'; -@import 'layer_panel/index'; +@import 'timeslider/index'; +@import 'edit_layer_panel/index'; @import 'right_side_controls/index'; @import 'toolbar_overlay/index'; @import 'mb_map/tooltip_control/features_tooltip/index'; diff --git a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx index 0774798eab46de..2b18d87f03c895 100644 --- a/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx +++ b/x-pack/plugins/maps/public/connected_components/add_layer_panel/view.tsx @@ -21,7 +21,7 @@ import { FlyoutBody } from './flyout_body'; import { LayerDescriptor } from '../../../common/descriptor_types'; import { LayerWizard } from '../../classes/layers/layer_wizard_registry'; -const ADD_LAYER_STEP_ID = 'ADD_LAYER_STEP_ID'; +export const ADD_LAYER_STEP_ID = 'ADD_LAYER_STEP_ID'; const ADD_LAYER_STEP_LABEL = i18n.translate('xpack.maps.addLayerPanel.addLayer', { defaultMessage: 'Add layer', }); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap similarity index 93% rename from x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap index bbec1643cb55df..24f15674a05043 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/__snapshots__/edit_layer_panel.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LayerPanel is rendered 1`] = ` +exports[`EditLayerPanel is rendered 1`] = ` - `; -exports[`LayerPanel should render empty panel when selectedLayer is null 1`] = `""`; +exports[`EditLayerPanel should render empty panel when selectedLayer is null 1`] = `""`; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/_index.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/_index.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/_index.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/_layer_panel.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/_layer_panel.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/_layer_panel.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx similarity index 83% rename from x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx index ae5368c4749859..fa5cdc77982b85 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.test.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.test.tsx @@ -29,12 +29,6 @@ jest.mock('./flyout_footer', () => ({ }, })); -jest.mock('./layer_errors', () => ({ - LayerErrors: () => { - return
mockLayerErrors
; - }, -})); - jest.mock('./layer_settings', () => ({ LayerSettings: () => { return
mockLayerSettings
; @@ -53,11 +47,11 @@ jest.mock('../../kibana_services', () => { }); import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; - -import { LayerPanel } from './view'; +import { shallow } from 'enzyme'; +import { ILayer } from '../../classes/layers/layer'; +import { EditLayerPanel } from './edit_layer_panel'; -const mockLayer = { +const mockLayer = ({ getId: () => { return '1'; }, @@ -79,7 +73,10 @@ const mockLayer = { renderSourceSettingsEditor: () => { return
mockSourceSettings
; }, -}; + hasErrors: () => { + return false; + }, +} as unknown) as ILayer; const defaultProps = { selectedLayer: mockLayer, @@ -87,9 +84,9 @@ const defaultProps = { updateSourceProp: () => {}, }; -describe('LayerPanel', () => { +describe('EditLayerPanel', () => { test('is rendered', async () => { - const component = shallowWithIntl(); + const component = shallow(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -100,7 +97,7 @@ describe('LayerPanel', () => { }); test('should render empty panel when selectedLayer is null', async () => { - const component = shallowWithIntl(); + const component = shallow(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx similarity index 64% rename from x-pack/plugins/maps/public/connected_components/layer_panel/view.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx index 9438d55ac18bc1..eb3c472252252a 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/edit_layer_panel.tsx @@ -5,15 +5,10 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React, { Component, Fragment } from 'react'; -import { FilterEditor } from './filter_editor'; -import { JoinEditor } from './join_editor'; -import { FlyoutFooter } from './flyout_footer'; -import { LayerErrors } from './layer_errors'; -import { LayerSettings } from './layer_settings'; -import { StyleSettings } from './style_settings'; import { + EuiCallOut, EuiIcon, EuiFlexItem, EuiTitle, @@ -26,20 +21,47 @@ import { EuiText, EuiLink, } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; +import { FilterEditor } from './filter_editor'; +import { JoinEditor, JoinField } from './join_editor'; +import { FlyoutFooter } from './flyout_footer'; +import { LayerSettings } from './layer_settings'; +import { StyleSettings } from './style_settings'; + import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; - +import { LAYER_TYPE } from '../../../common/constants'; import { getData, getCore } from '../../kibana_services'; +import { ILayer } from '../../classes/layers/layer'; +import { IVectorLayer } from '../../classes/layers/vector_layer'; +import { ImmutableSourceProperty, OnSourceChangeArgs } from '../../classes/sources/source'; +import { IField } from '../../classes/fields/field'; const localStorage = new Storage(window.localStorage); -export class LayerPanel extends React.Component { - state = { +export interface Props { + selectedLayer?: ILayer; + updateSourceProp: ( + layerId: string, + propName: string, + value: unknown, + newLayerType?: LAYER_TYPE + ) => void; +} + +interface State { + displayName: string; + immutableSourceProps: ImmutableSourceProperty[]; + leftJoinFields: JoinField[]; + supportsFitToBounds: boolean; +} + +export class EditLayerPanel extends Component { + private _isMounted = false; + state: State = { displayName: '', immutableSourceProps: [], - leftJoinFields: null, + leftJoinFields: [], supportsFitToBounds: false, }; @@ -89,14 +111,19 @@ export class LayerPanel extends React.Component { }; async _loadLeftJoinFields() { - if (!this.props.selectedLayer || !this.props.selectedLayer.showJoinEditor()) { + if ( + !this.props.selectedLayer || + !this.props.selectedLayer.showJoinEditor() || + (this.props.selectedLayer as IVectorLayer).getLeftJoinFields === undefined + ) { return; } - let leftJoinFields; + let leftJoinFields: JoinField[] = []; try { - const leftFieldsInstances = await this.props.selectedLayer.getLeftJoinFields(); - const leftFieldPromises = leftFieldsInstances.map(async (field) => { + const leftFieldsInstances = await (this.props + .selectedLayer as IVectorLayer).getLeftJoinFields(); + const leftFieldPromises = leftFieldsInstances.map(async (field: IField) => { return { name: field.getName(), label: await field.getLabel(), @@ -104,22 +131,42 @@ export class LayerPanel extends React.Component { }); leftJoinFields = await Promise.all(leftFieldPromises); } catch (error) { - leftJoinFields = []; + // ignore exceptions getting fields, will bubble up in layer errors panel } if (this._isMounted) { this.setState({ leftJoinFields }); } } - _onSourceChange = (...args) => { + _onSourceChange = (...args: OnSourceChangeArgs[]) => { for (let i = 0; i < args.length; i++) { const { propName, value, newLayerType } = args[i]; - this.props.updateSourceProp(this.props.selectedLayer.getId(), propName, value, newLayerType); + this.props.updateSourceProp(this.props.selectedLayer!.getId(), propName, value, newLayerType); } }; + _renderLayerErrors() { + if (!this.props.selectedLayer || !this.props.selectedLayer.hasErrors()) { + return null; + } + + return ( + + +

{this.props.selectedLayer.getErrors()}

+
+ +
+ ); + } + _renderFilterSection() { - if (!this.props.selectedLayer.supportsElasticsearchFilters()) { + if (!this.props.selectedLayer || !this.props.selectedLayer.supportsElasticsearchFilters()) { return null; } @@ -134,7 +181,7 @@ export class LayerPanel extends React.Component { } _renderJoinSection() { - if (!this.props.selectedLayer.showJoinEditor()) { + if (!this.props.selectedLayer || !this.props.selectedLayer.showJoinEditor()) { return null; } @@ -153,29 +200,29 @@ export class LayerPanel extends React.Component { } _renderSourceProperties() { - return this.state.immutableSourceProps.map(({ label, value, link }) => { - function renderValue() { - if (link) { - return ( - - {value} - - ); + return this.state.immutableSourceProps.map( + ({ label, value, link }: ImmutableSourceProperty) => { + function renderValue() { + if (link) { + return ( + + {value} + + ); + } + return {value}; } - return {value}; + return ( +

+ {label} {renderValue()} +

+ ); } - return ( -

- {label} {renderValue()} -

- ); - }); + ); } render() { - const { selectedLayer } = this.props; - - if (!selectedLayer) { + if (!this.props.selectedLayer) { return null; } @@ -192,7 +239,7 @@ export class LayerPanel extends React.Component { - + @@ -218,10 +265,10 @@ export class LayerPanel extends React.Component {
- + {this._renderLayerErrors()} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/filter_editor/_filter_editor.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/_filter_editor.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/filter_editor/_filter_editor.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/filter_editor/filter_editor.tsx similarity index 87% rename from x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/filter_editor/filter_editor.tsx index 654ce5a4e9ddbf..6e258e679b96f9 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/filter_editor/filter_editor.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/filter_editor/filter_editor.tsx @@ -21,12 +21,28 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import type { IndexPattern, Query } from 'src/plugins/data/public'; +import { APP_ID } from '../../../../common/constants'; import { getIndexPatternService, getData } from '../../../kibana_services'; import { GlobalFilterCheckbox } from '../../../components/global_filter_checkbox'; import { GlobalTimeCheckbox } from '../../../components/global_time_checkbox'; +import { ILayer } from '../../../classes/layers/layer'; -export class FilterEditor extends Component { - state = { +export interface Props { + layer: ILayer; + setLayerQuery: (id: string, query: Query) => void; + updateSourceProp: (layerId: string, propName: string, value: unknown) => void; +} + +interface State { + isPopoverOpen: boolean; + indexPatterns: IndexPattern[]; + isSourceTimeAware: boolean; +} + +export class FilterEditor extends Component { + private _isMounted = false; + state: State = { isPopoverOpen: false, indexPatterns: [], isSourceTimeAware: false, @@ -45,7 +61,7 @@ export class FilterEditor extends Component { async _loadIndexPatterns() { // Filter only effects source so only load source indices. const indexPatternIds = this.props.layer.getSource().getIndexPatternIds(); - const indexPatterns = []; + const indexPatterns: IndexPattern[] = []; const getIndexPatternPromises = indexPatternIds.map(async (indexPatternId) => { try { const indexPattern = await getIndexPatternService().get(indexPatternId); @@ -81,16 +97,19 @@ export class FilterEditor extends Component { this.setState({ isPopoverOpen: false }); }; - _onQueryChange = ({ query }) => { + _onQueryChange = ({ query }: { query?: Query }) => { + if (!query) { + return; + } this.props.setLayerQuery(this.props.layer.getId(), query); this._close(); }; - _onApplyGlobalQueryChange = (applyGlobalQuery) => { + _onApplyGlobalQueryChange = (applyGlobalQuery: boolean) => { this.props.updateSourceProp(this.props.layer.getId(), 'applyGlobalQuery', applyGlobalQuery); }; - _onApplyGlobalTimeChange = (applyGlobalTime) => { + _onApplyGlobalTimeChange = (applyGlobalTime: boolean) => { this.props.updateSourceProp(this.props.layer.getId(), 'applyGlobalTime', applyGlobalTime); }; @@ -109,6 +128,7 @@ export class FilterEditor extends Component { >
) { return { - setLayerQuery: (layerId, query) => { + setLayerQuery: (layerId: string, query: Query) => { dispatch(setLayerQuery(layerId, query)); }, - updateSourceProp: (id, propName, value) => dispatch(updateSourceProp(id, propName, value)), + updateSourceProp: (id: string, propName: string, value: unknown) => + dispatch(updateSourceProp(id, propName, value)), }; } diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/flyout_footer/flyout_footer.tsx similarity index 92% rename from x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/flyout_footer/flyout_footer.tsx index 7070a875715f47..b55f9488e1f767 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/flyout_footer/view.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/flyout_footer/flyout_footer.tsx @@ -10,12 +10,19 @@ import React from 'react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +export interface Props { + cancelLayerPanel: () => void; + saveLayerEdits: () => void; + removeLayer: () => void; + hasStateChanged: boolean; +} + export const FlyoutFooter = ({ cancelLayerPanel, saveLayerEdits, removeLayer, hasStateChanged, -}) => { +}: Props) => { const removeBtn = ( { +const mapDispatchToProps = (dispatch: ThunkDispatch) => { return { cancelLayerPanel: () => { dispatch(updateFlyout(FLYOUT_STATE.NONE)); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/index.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts similarity index 53% rename from x-pack/plugins/maps/public/connected_components/layer_panel/index.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts index d47ebc98a3fe3b..b78ffb3874e304 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/index.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/index.ts @@ -5,12 +5,16 @@ * 2.0. */ +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; -import { LayerPanel } from './view'; +import { EditLayerPanel } from './edit_layer_panel'; +import { LAYER_TYPE } from '../../../common/constants'; import { getSelectedLayer } from '../../selectors/map_selectors'; import { updateSourceProp } from '../../actions'; +import { MapStoreState } from '../../reducers/store'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { const selectedLayer = getSelectedLayer(state); return { key: selectedLayer ? `${selectedLayer.getId()}${selectedLayer.showJoinEditor()}` : '', @@ -18,12 +22,12 @@ function mapStateToProps(state = {}) { }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { - updateSourceProp: (id, propName, value, newLayerType) => + updateSourceProp: (id: string, propName: string, value: unknown, newLayerType?: LAYER_TYPE) => dispatch(updateSourceProp(id, propName, value, newLayerType)), }; } -const connectedLayerPanel = connect(mapStateToProps, mapDispatchToProps)(LayerPanel); -export { connectedLayerPanel as LayerPanel }; +const connected = connect(mapStateToProps, mapDispatchToProps)(EditLayerPanel); +export { connected as EditLayerPanel }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/__snapshots__/join_editor.test.tsx.snap diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/index.ts similarity index 96% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/index.ts index fb8e27205368a9..7971017a3d52b6 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/index.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/index.ts @@ -31,3 +31,4 @@ function mapDispatchToProps(dispatch: ThunkDispatch void; } diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/__snapshots__/metrics_expression.test.js.snap diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/_join.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/_join.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/_join.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.js similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/join_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.test.js diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/where_expression.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/where_expression.js diff --git a/x-pack/plugins/maps/public/connected_components/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 similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/__snapshots__/attribution_form_row.test.tsx.snap diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/_attribution_form_row.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/_attribution_form_row.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/_attribution_form_row.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/_attribution_form_row.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/_attribution_popover.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/_attribution_popover.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/_attribution_popover.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/_attribution_popover.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/_index.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/_index.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/_index.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/_index.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/attribution_form_row.test.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.test.tsx similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/attribution_form_row.test.tsx rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.test.tsx diff --git a/x-pack/plugins/maps/public/connected_components/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 similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/attribution_form_row.tsx rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_form_row.tsx diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/attribution_popover.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/attribution_popover.tsx rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/attribution_popover.tsx diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/index.tsx similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/index.tsx rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/index.tsx diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/layer_settings.tsx similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.tsx rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/layer_settings/layer_settings.tsx diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/_style_settings.scss similarity index 100% rename from x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/_style_settings.scss rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/_style_settings.scss diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/index.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/index.ts similarity index 62% rename from x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/index.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/index.ts index a25477880e4654..d52689cda141af 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/index.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/index.ts @@ -5,20 +5,24 @@ * 2.0. */ +import { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { StyleSettings } from './style_settings'; import { getSelectedLayer } from '../../../selectors/map_selectors'; import { updateLayerStyleForSelectedLayer } from '../../../actions'; +import { MapStoreState } from '../../../reducers/store'; +import { StyleDescriptor } from '../../../../common/descriptor_types'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { return { - layer: getSelectedLayer(state), + layer: getSelectedLayer(state)!, }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { - updateStyleDescriptor: (styleDescriptor) => { + updateStyleDescriptor: (styleDescriptor: StyleDescriptor) => { dispatch(updateLayerStyleForSelectedLayer(styleDescriptor)); }, }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx similarity index 78% rename from x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js rename to x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx index b37e58925195c1..06e38079403d12 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/style_settings/style_settings.js +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/style_settings/style_settings.tsx @@ -10,8 +10,15 @@ import React, { Fragment } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPanel, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { StyleDescriptor } from '../../../../common/descriptor_types'; +import { ILayer } from '../../../classes/layers/layer'; -export function StyleSettings({ layer, updateStyleDescriptor }) { +export interface Props { + layer: ILayer; + updateStyleDescriptor: (styleDescriptor: StyleDescriptor) => void; +} + +export function StyleSettings({ layer, updateStyleDescriptor }: Props) { const settingsEditor = layer.renderStyleEditor(updateStyleDescriptor); if (!settingsEditor) { diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap deleted file mode 100644 index 1fdc3a1bfdf178..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/__snapshots__/layer_errors.test.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Should render errors when layer has errors 1`] = ` - - -

- simulated layer error -

-
- -
-`; - -exports[`should render nothing when layer has no errors 1`] = `""`; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js deleted file mode 100644 index 932034cc19da62..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/index.js +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { connect } from 'react-redux'; -import { LayerErrors } from './layer_errors'; -import { getSelectedLayer } from '../../../selectors/map_selectors'; - -function mapStateToProps(state = {}) { - return { - layer: getSelectedLayer(state), - }; -} - -const connectedLayerErrors = connect(mapStateToProps, null)(LayerErrors); -export { connectedLayerErrors as LayerErrors }; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js deleted file mode 100644 index 13c802faf79e76..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; - -export function LayerErrors({ layer }) { - if (!layer.hasErrors()) { - return null; - } - - return ( - - -

{layer.getErrors()}

-
- -
- ); -} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js deleted file mode 100644 index 333a1b2bd1e4ee..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_errors/layer_errors.test.js +++ /dev/null @@ -1,36 +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 { shallow } from 'enzyme'; - -import { LayerErrors } from './layer_errors'; - -test('Should render errors when layer has errors', () => { - const mockLayer = { - hasErrors: () => { - return true; - }, - getErrors: () => { - return 'simulated layer error'; - }, - }; - const component = shallow(); - - expect(component).toMatchSnapshot(); -}); - -test('should render nothing when layer has no errors', () => { - const mockLayer = { - hasErrors: () => { - return false; - }, - }; - const component = shallow(); - - expect(component).toMatchSnapshot(); -}); diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/view.d.ts b/x-pack/plugins/maps/public/connected_components/layer_panel/view.d.ts deleted file mode 100644 index c1c55ebe6040ba..00000000000000 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/view.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -import { LAYER_TYPE } from '../../../common/constants'; - -export type OnSourceChangeArgs = { - propName: string; - value: unknown; - newLayerType?: LAYER_TYPE; -}; diff --git a/x-pack/plugins/maps/public/connected_components/map_container/index.ts b/x-pack/plugins/maps/public/connected_components/map_container/index.ts index cda96792fc6d68..408137b6f23b35 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/index.ts +++ b/x-pack/plugins/maps/public/connected_components/map_container/index.ts @@ -13,6 +13,7 @@ import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors' import { triggerRefreshTimer, cancelAllInFlightRequests, exitFullScreen } from '../../actions'; import { areLayersLoaded, + getLayerList, getRefreshConfig, getMapInitError, getMapSettings, @@ -30,6 +31,7 @@ function mapStateToProps(state: MapStoreState) { mapInitError: getMapInitError(state), indexPatternIds: getQueryableUniqueIndexPatternIds(state), settings: getMapSettings(state), + layerList: getLayerList(state), }; } diff --git a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx index 851fd583b4251f..02374932a4c703 100644 --- a/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_container/map_container.tsx @@ -15,9 +15,9 @@ import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { MBMap } from '../mb_map'; import { RightSideControls } from '../right_side_controls'; +import { Timeslider } from '../timeslider'; import { ToolbarOverlay } from '../toolbar_overlay'; -// @ts-expect-error -import { LayerPanel } from '../layer_panel'; +import { EditLayerPanel } from '../edit_layer_panel'; import { AddLayerPanel } from '../add_layer_panel'; import { ExitFullScreenButton } from '../../../../../../src/plugins/kibana_react/public'; import { getIndexPatternsFromIds } from '../../index_pattern_util'; @@ -30,6 +30,7 @@ import { registerLayerWizards } from '../../classes/layers/load_layer_wizards'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; import { MapRefreshConfig } from '../../../common/descriptor_types'; +import { ILayer } from '../../classes/layers/layer'; const RENDER_COMPLETE_EVENT = 'renderComplete'; @@ -51,12 +52,15 @@ export interface Props { title?: string; description?: string; settings: MapSettings; + layerList: ILayer[]; } interface State { isInitialLoadRenderTimeoutComplete: boolean; domId: string; geoFields: GeoFieldWithIndex[]; + showFitToBoundsButton: boolean; + showTimesliderButton: boolean; } export class MapContainer extends Component { @@ -71,16 +75,22 @@ export class MapContainer extends Component { isInitialLoadRenderTimeoutComplete: false, domId: uuid(), geoFields: [], + showFitToBoundsButton: false, + showTimesliderButton: false, }; componentDidMount() { this._isMounted = true; this._setRefreshTimer(); + this._loadShowFitToBoundsButton(); + this._loadShowTimesliderButton(); registerLayerWizards(); } componentDidUpdate() { this._setRefreshTimer(); + this._loadShowFitToBoundsButton(); + this._loadShowTimesliderButton(); if (this.props.areLayersLoaded && !this._isInitalLoadRenderTimerStarted) { this._isInitalLoadRenderTimerStarted = true; this._startInitialLoadRenderTimer(); @@ -112,7 +122,36 @@ export class MapContainer extends Component { } }; - _loadGeoFields = async (nextIndexPatternIds: string[]) => { + async _loadShowFitToBoundsButton() { + const promises = this.props.layerList.map(async (layer) => { + return await layer.isFittable(); + }); + const showFitToBoundsButton = (await Promise.all(promises)).some((isFittable) => isFittable); + if (this._isMounted && this.state.showFitToBoundsButton !== showFitToBoundsButton) { + this.setState({ showFitToBoundsButton }); + } + } + + async _loadShowTimesliderButton() { + if (!this.props.settings.showTimesliderToggleButton) { + if (this.state.showTimesliderButton) { + this.setState({ showTimesliderButton: false }); + } + return; + } + + const promises = this.props.layerList.map(async (layer) => { + return await layer.isFilteredByGlobalTime(); + }); + const showTimesliderButton = (await Promise.all(promises)).some( + (isFilteredByGlobalTime) => isFilteredByGlobalTime + ); + if (this._isMounted && this.state.showTimesliderButton !== showTimesliderButton) { + this.setState({ showTimesliderButton }); + } + } + + async _loadGeoFields(nextIndexPatternIds: string[]) { if (_.isEqual(nextIndexPatternIds, this._prevIndexPatternIds)) { // all ready loaded index pattern ids return; @@ -144,7 +183,7 @@ export class MapContainer extends Component { } this.setState({ geoFields }); - }; + } _setRefreshTimer = () => { const { isPaused, interval } = this.props.refreshConfig; @@ -222,7 +261,7 @@ export class MapContainer extends Component { if (flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD) { flyoutPanel = ; } else if (flyoutDisplay === FLYOUT_STATE.LAYER_PANEL) { - flyoutPanel = ; + flyoutPanel = ; } else if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { flyoutPanel = ; } @@ -259,11 +298,15 @@ export class MapContainer extends Component { geoFields={this.state.geoFields} getFilterActions={getFilterActions} getActionContext={getActionContext} + showFitToBoundsButton={this.state.showFitToBoundsButton} + showTimesliderButton={this.state.showTimesliderButton} /> )}
+ + {}; 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 355e49564620dd..877de10e113834 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 @@ -7,7 +7,7 @@ import _ from 'lodash'; import React, { Component } from 'react'; -import type { Map as MapboxMap, MapboxOptions, MapMouseEvent } from 'mapbox-gl'; +import type { Map as MapboxMap, MapboxOptions, MapMouseEvent } from '@kbn/mapbox-gl'; // @ts-expect-error import { spritesheet } from '@elastic/maki'; @@ -418,11 +418,11 @@ export class MBMap extends Component { }; render() { - let drawControl; + let drawFilterControl; let tooltipControl; let scaleControl; if (this.state.mbMap) { - drawControl = this.props.addFilters ? ( + drawFilterControl = this.props.addFilters ? ( ) : null; tooltipControl = !this.props.settings.disableTooltipControl ? ( @@ -447,7 +447,7 @@ export class MBMap extends Component { ref={this._setContainerRef} data-test-subj="mapContainer" > - {drawControl} + {drawFilterControl} {scaleControl} {tooltipControl}
diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.test.tsx index bd735d5cd5183a..0eb3ff2de16e8d 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; import { ScaleControl } from './scale_control'; -import { LngLat, LngLatBounds, Map as MapboxMap, PointLike } from 'mapbox-gl'; +import type { LngLat, LngLatBounds, Map as MapboxMap, PointLike } from '@kbn/mapbox-gl'; const CLIENT_HEIGHT_PIXELS = 1200; const DISTANCE_METERS = 87653; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.tsx index 68f37721bdb1f0..d762f17b9b8982 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/scale_control/scale_control.tsx @@ -8,7 +8,7 @@ import { i18n } from '@kbn/i18n'; import classNames from 'classnames'; import React, { Component } from 'react'; -import { Map as MapboxMap } from 'mapbox-gl'; +import type { Map as MapboxMap } from '@kbn/mapbox-gl'; const MAX_WIDTH = 110; interface Props { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts index 6be53cfb1349a1..596ba793db708f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.test.ts @@ -8,7 +8,7 @@ /* eslint-disable max-classes-per-file */ import _ from 'lodash'; -import { Map as MbMap, Layer as MbLayer, Style as MbStyle } from 'mapbox-gl'; +import type { Map as MbMap, AnyLayer as MbLayer, Style as MbStyle } from '@kbn/mapbox-gl'; import { getIsTextLayer, syncLayerOrder } from './sort_layers'; import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants'; import { ILayer } from '../../classes/layers/layer'; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts index 8c16c13bfcc90e..927c6819351c27 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/sort_layers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MbMap, Layer as MbLayer } from 'mapbox-gl'; +import type { Map as MbMap, Layer as MbLayer } from '@kbn/mapbox-gl'; import { ILayer } from '../../classes/layers/layer'; // "Layer" is overloaded and can mean the following diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts index 223efae6576012..6b47fe3e6e650f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.test.ts @@ -7,7 +7,7 @@ // eslint-disable-next-line max-classes-per-file import { TileStatusTracker } from './tile_status_tracker'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { ILayer } from '../../classes/layers/layer'; class MockMbMap { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts index be946a12fe225c..fc99cd3067d0b9 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tile_status_tracker.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Map as MapboxMap, MapSourceDataEvent } from 'mapbox-gl'; +import type { Map as MapboxMap, MapSourceDataEvent } from '@kbn/mapbox-gl'; import _ from 'lodash'; import { ILayer } from '../../classes/layers/layer'; import { SPATIAL_FILTERS_LAYER_ID } from '../../../common/constants'; 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 451150fabb064c..ac6e3cfcccf4e6 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 @@ -14,7 +14,7 @@ jest.mock('./tooltip_popover', () => ({ import sinon from 'sinon'; import React from 'react'; import { mount, shallow } from 'enzyme'; -import { Map as MbMap, MapMouseEvent, MapboxGeoJSONFeature } from 'mapbox-gl'; +import type { Map as MbMap, MapMouseEvent, MapboxGeoJSONFeature } from '@kbn/mapbox-gl'; import { TooltipControl } from './tooltip_control'; import { IVectorLayer } from '../../../classes/layers/vector_layer'; 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 e8c3a46430cb93..09dd9ee4f51d90 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 @@ -14,7 +14,7 @@ import { MapboxGeoJSONFeature, MapMouseEvent, Point as MbPoint, -} from 'mapbox-gl'; +} from '@kbn/mapbox-gl'; import uuid from 'uuid/v4'; import { Geometry } from 'geojson'; import { Filter } from 'src/plugins/data/public'; diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.test.tsx index 22223df3660116..002ec09e68c2fc 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.test.tsx @@ -14,7 +14,7 @@ jest.mock('./features_tooltip/features_tooltip', () => ({ import sinon from 'sinon'; import React from 'react'; import { mount, shallow } from 'enzyme'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { TooltipPopover } from './tooltip_popover'; // mutable map state diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx index a3fcc0ea6a07a6..0b7ba3468d30ce 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/tooltip_control/tooltip_popover.tsx @@ -7,7 +7,7 @@ import React, { Component, RefObject } from 'react'; import { EuiPopover, EuiText } from '@elastic/eui'; -import { Map as MbMap } from 'mapbox-gl'; +import type { Map as MbMap } from '@kbn/mapbox-gl'; import { GeoJsonProperties, Geometry } from 'geojson'; import { Filter } from 'src/plugins/data/public'; import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; diff --git a/x-pack/plugins/maps/public/connected_components/timeslider/_index.scss b/x-pack/plugins/maps/public/connected_components/timeslider/_index.scss new file mode 100644 index 00000000000000..6e169615750955 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/timeslider/_index.scss @@ -0,0 +1,89 @@ +.mapTimeslider { + @include euiBottomShadowLarge; + position: fixed; + left: 50%; + bottom: $euiSize; + transform: translateX(-50%); + min-width: 530px; + z-index: 99999; + background: $euiColorEmptyShade; + padding: $euiSizeL $euiSizeXL; + border-radius: $euiSize; + + .euiRangeTick { + overflow-x: hidden; + text-overflow: ellipsis; + font-size: $euiFontSizeXS; + position: relative; + padding-top: $euiSize; + color: $euiColorDarkShade; + + &::before { + @include size($euiSizeXS); + width: 3px; + height: $euiSizeS; + content: ''; + background-color: $euiColorLightShade; + position: absolute; + top: $euiSizeXS; + left: calc(50% - #{($euiSizeXS/2)}); + } + + &--isCustom { + position: absolute; + transform: translateX(-50%); + } + + &:enabled:hover, + &:focus, + &--selected { + color: $euiColorPrimary; + } + + &--selected { + font-weight: $euiFontWeightMedium; + } + + &:disabled { + cursor: not-allowed; + } + } +} + +.mapTimeslider__row { + display: flex; + + & + & { + margin-top: $euiSizeS; + } + + > * { + align-items: center; + } +} + +.mapTimeslider__close { + position: absolute; + top: $euiSizeXS; + right: $euiSizeXS; +} + +.mapTimeslider__timeWindow { + display: flex; + flex: 1; + margin-right: $euiSizeS; + font-size: $euiFontSizeS; +} + +.mapTimeslider__controls { + margin-left: $euiSizeS; +} + +.mapTimeslider__innerPanel { + display: inline-flex; + // background: $euiColorLightestShade; + border-radius: $euiBorderRadiusSmall; + padding: $euiSizeXS; + display: inline-flex; + align-items: center; +} diff --git a/x-pack/plugins/maps/public/connected_components/timeslider/index.ts b/x-pack/plugins/maps/public/connected_components/timeslider/index.ts new file mode 100644 index 00000000000000..90d29f9ae06358 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/timeslider/index.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 { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { connect } from 'react-redux'; +import { Timeslider } from './timeslider'; +import { closeTimeslider, setQuery } from '../../actions'; +import { getTimeFilters } from '../../selectors/map_selectors'; +import { getIsTimesliderOpen } from '../../selectors/ui_selectors'; +import { MapStoreState } from '../../reducers/store'; +import { Timeslice } from '../../../common/descriptor_types'; + +function mapStateToProps(state: MapStoreState) { + return { + isTimesliderOpen: getIsTimesliderOpen(state), + timeRange: getTimeFilters(state), + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch) { + return { + closeTimeslider: () => { + dispatch(closeTimeslider()); + }, + setTimeslice: (timeslice: Timeslice) => { + dispatch( + setQuery({ + forceRefresh: false, + timeslice, + }) + ); + }, + }; +} + +const connected = connect(mapStateToProps, mapDispatchToProps)(Timeslider); +export { connected as Timeslider }; diff --git a/x-pack/plugins/maps/public/connected_components/timeslider/time_utils.test.ts b/x-pack/plugins/maps/public/connected_components/timeslider/time_utils.test.ts new file mode 100644 index 00000000000000..16973b5a84478c --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/timeslider/time_utils.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getInterval } from './time_utils'; + +describe('getInterval', () => { + test('should provide interval of 1 day for 7 day range', () => { + expect(getInterval(1617630946622, 1618235746622)).toBe(86400000); + }); + + test('should provide interval of 3 hours for 24 hour range', () => { + expect(getInterval(1618150382531, 1618236782531)).toBe(10800000); + }); + + test('should provide interval of 90 minues for 12 hour range', () => { + expect(getInterval(1618193892632, 1618237092632)).toBe(5400000); + }); + + test('should provide interval of 30 minues for 4 hour range', () => { + expect(getInterval(1618222509189, 1618236909189)).toBe(1800000); + }); + + test('should provide interval of 10 minues for 1 hour range', () => { + expect(getInterval(1618233266459, 1618236866459)).toBe(600000); + }); +}); diff --git a/x-pack/plugins/maps/public/connected_components/timeslider/time_utils.ts b/x-pack/plugins/maps/public/connected_components/timeslider/time_utils.ts new file mode 100644 index 00000000000000..7195851aafe2c0 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/timeslider/time_utils.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import moment from 'moment-timezone'; +import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; +import { calcAutoIntervalNear } from '../../../../../../src/plugins/data/common'; +import { getUiSettings } from '../../kibana_services'; + +function getTimezone() { + const detectedTimezone = moment.tz.guess(); + const dateFormatTZ = getUiSettings().get('dateFormat:tz', 'Browser'); + + return dateFormatTZ === 'Browser' ? detectedTimezone : dateFormatTZ; +} + +function getScaledDateFormat(interval: number): string { + if (interval >= moment.duration(1, 'y').asMilliseconds()) { + return 'YYYY'; + } + + if (interval >= moment.duration(1, 'd').asMilliseconds()) { + return 'MMM D'; + } + + if (interval >= moment.duration(6, 'h').asMilliseconds()) { + return 'Do HH'; + } + + if (interval >= moment.duration(1, 'h').asMilliseconds()) { + return 'HH:mm'; + } + + if (interval >= moment.duration(1, 'm').asMilliseconds()) { + return 'HH:mm'; + } + + if (interval >= moment.duration(1, 's').asMilliseconds()) { + return 'mm:ss'; + } + + return 'ss.SSS'; +} + +export function epochToKbnDateFormat(epoch: number): string { + const dateFormat = getUiSettings().get('dateFormat', 'MMM D, YYYY @ HH:mm:ss.SSS'); + const timezone = getTimezone(); + return moment.tz(epoch, timezone).format(dateFormat); +} + +export function getInterval(min: number, max: number, steps = 6): number { + const duration = max - min; + let interval = calcAutoIntervalNear(steps, duration).asMilliseconds(); + // Sometimes auto interval is not quite right and returns 2X or 3X requested ticks + // Adjust the interval to get closer to the requested number of ticks + const actualSteps = duration / interval; + if (actualSteps > steps * 1.5) { + const factor = Math.round(actualSteps / steps); + interval *= factor; + } else if (actualSteps < 5) { + interval *= 0.5; + } + return interval; +} + +export function getTicks(min: number, max: number, interval: number): EuiRangeTick[] { + const format = getScaledDateFormat(interval); + const timezone = getTimezone(); + + let tick = Math.ceil(min / interval) * interval; + const ticks: EuiRangeTick[] = []; + while (tick < max) { + ticks.push({ + value: tick, + label: moment.tz(tick, timezone).format(format), + }); + tick += interval; + } + + return ticks; +} diff --git a/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx b/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx new file mode 100644 index 00000000000000..0b7bcb115eb954 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/timeslider/timeslider.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import _ from 'lodash'; +import React, { Component } from 'react'; +import { EuiButtonIcon, EuiDualRange, EuiText } from '@elastic/eui'; +import { EuiRangeTick } from '@elastic/eui/src/components/form/range/range_ticks'; +import { i18n } from '@kbn/i18n'; +import { epochToKbnDateFormat, getInterval, getTicks } from './time_utils'; +import { TimeRange } from '../../../../../../src/plugins/data/common'; +import { getTimeFilter } from '../../kibana_services'; +import { Timeslice } from '../../../common/descriptor_types'; + +export interface Props { + closeTimeslider: () => void; + setTimeslice: (timeslice: Timeslice) => void; + isTimesliderOpen: boolean; + timeRange: TimeRange; +} + +interface State { + max: number; + min: number; + range: number; + timeslice: [number, number]; + ticks: EuiRangeTick[]; +} + +function prettyPrintTimeslice(timeslice: [number, number]) { + return `${epochToKbnDateFormat(timeslice[0])} - ${epochToKbnDateFormat(timeslice[1])}`; +} + +// Why Timeslider and KeyedTimeslider? +// Using react 'key' property to ensure new KeyedTimeslider instance whenever props.timeRange changes +export function Timeslider(props: Props) { + return props.isTimesliderOpen ? ( + + ) : null; +} + +class KeyedTimeslider extends Component { + private _isMounted: boolean = false; + + constructor(props: Props) { + super(props); + const timeRangeBounds = getTimeFilter().calculateBounds(props.timeRange); + if (timeRangeBounds.min === undefined || timeRangeBounds.max === undefined) { + throw new Error( + 'Unable to create Timeslider component, timeRangeBounds min or max are undefined' + ); + } + const min = timeRangeBounds.min.valueOf(); + const max = timeRangeBounds.max.valueOf(); + const interval = getInterval(min, max); + const timeslice: [number, number] = [min, max]; + + this.state = { + max, + min, + range: interval, + ticks: getTicks(min, max, interval), + timeslice, + }; + } + + componentWillUnmount() { + this._isMounted = false; + } + + componentDidMount() { + this._isMounted = true; + // auto-select range between first tick and second tick + this._onChange([this.state.ticks[0].value, this.state.ticks[1].value]); + } + + _doesTimesliceCoverTimerange() { + return this.state.timeslice[0] === this.state.min && this.state.timeslice[1] === this.state.max; + } + + _onDualControlChange = (value: [number | string, number | string]) => { + this.setState({ range: (value[1] as number) - (value[0] as number) }, () => { + this._onChange(value as [number, number]); + }); + }; + + _onChange = (value: [number, number]) => { + this.setState({ + timeslice: value, + }); + this._propagateChange(value); + }; + + _onNext = () => { + const from = + this._doesTimesliceCoverTimerange() || this.state.timeslice[1] === this.state.max + ? this.state.ticks[0].value + : this.state.timeslice[1]; + const to = from + this.state.range; + this._onChange([from, to <= this.state.max ? to : this.state.max]); + }; + + _onPrevious = () => { + const to = + this._doesTimesliceCoverTimerange() || this.state.timeslice[0] === this.state.min + ? this.state.ticks[this.state.ticks.length - 1].value + : this.state.timeslice[0]; + const from = to - this.state.range; + this._onChange([from < this.state.min ? this.state.min : from, to]); + }; + + _propagateChange = _.debounce((value: [number, number]) => { + if (this._isMounted) { + this.props.setTimeslice({ from: value[0], to: value[1] }); + } + }, 300); + + render() { + return ( +
+
+ + +
+ {prettyPrintTimeslice(this.state.timeslice)} +
+ +
+
+ + +
+
+
+ +
+ +
+
+ ); + } +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap index 506767fcd47065..168a070b077441 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/__snapshots__/toolbar_overlay.test.tsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Must render zoom tools 1`] = ` +exports[`Should only show set view control 1`] = ` - - - `; -exports[`Must zoom tools and draw filter tools 1`] = ` +exports[`Should show all controls 1`] = ` + + + `; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx index 64e163cd96a92d..f975bc293d8239 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx @@ -9,68 +9,27 @@ import React from 'react'; import { EuiButtonIcon, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ILayer } from '../../../classes/layers/layer'; export interface Props { - layerList: ILayer[]; fitToBounds: () => void; } -interface State { - canFit: boolean; -} - -export class FitToData extends React.Component { - _isMounted: boolean = false; - - state = { canFit: false }; - - componentDidMount(): void { - this._isMounted = true; - this._loadCanFit(); - } - - componentWillUnmount(): void { - this._isMounted = false; - } - - componentDidUpdate(): void { - this._loadCanFit(); - } - - async _loadCanFit() { - const promises = this.props.layerList.map(async (layer) => { - return await layer.isFittable(); - }); - const canFit = (await Promise.all(promises)).some((isFittable) => isFittable); - if (this._isMounted && this.state.canFit !== canFit) { - this.setState({ - canFit, - }); - } - } - - render() { - if (!this.state.canFit) { - return null; - } - - return ( - - - - ); - } +export function FitToData(props: Props) { + return ( + + + + ); } diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts index 99fddff5153b4f..b4322c93097f0e 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/index.ts @@ -10,13 +10,10 @@ import { ThunkDispatch } from 'redux-thunk'; import { connect } from 'react-redux'; import { MapStoreState } from '../../../reducers/store'; import { fitToDataBounds } from '../../../actions'; -import { getLayerList } from '../../../selectors/map_selectors'; import { FitToData } from './fit_to_data'; function mapStateToProps(state: MapStoreState) { - return { - layerList: getLayerList(state), - }; + return {}; } function mapDispatchToProps(dispatch: ThunkDispatch) { diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_circle_icon.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_circle_icon.tsx new file mode 100644 index 00000000000000..1a39072d0842fd --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_circle_icon.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; + +export const VectorCircleIcon: FunctionComponent = () => ( + + + +); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_line_icon.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_line_icon.tsx new file mode 100644 index 00000000000000..0e4baf536e7007 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_line_icon.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; + +export const VectorLineIcon: FunctionComponent = () => ( + + + + +); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_square_icon.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_square_icon.tsx new file mode 100644 index 00000000000000..4734a4feebc8fa --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/icons/vector_square_icon.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; + +export const VectorSquareIcon: FunctionComponent = () => ( + + + +); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/clock_play_icon.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/clock_play_icon.tsx new file mode 100644 index 00000000000000..bc24889d2ec2c0 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/clock_play_icon.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; + +interface Props { + title?: string; + titleId?: string; +} + +export const ClockPlayIcon: FunctionComponent = ({ title, titleId, ...props }) => ( + + {title ? {title} : null} + + + + +); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/index.ts b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/index.ts new file mode 100644 index 00000000000000..b31747f7e6a9f7 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/index.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 { AnyAction } from 'redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { connect } from 'react-redux'; +import { TimesliderToggleButton } from './timeslider_toggle_button'; +import { closeTimeslider, openTimeslider } from '../../../actions'; +import { getIsTimesliderOpen } from '../../../selectors/ui_selectors'; +import { MapStoreState } from '../../../reducers/store'; + +function mapStateToProps(state: MapStoreState) { + return { + isTimesliderOpen: getIsTimesliderOpen(state), + }; +} + +function mapDispatchToProps(dispatch: ThunkDispatch) { + return { + closeTimeslider: () => { + dispatch(closeTimeslider()); + }, + openTimeslider: () => { + dispatch(openTimeslider()); + }, + }; +} + +const connected = connect(mapStateToProps, mapDispatchToProps)(TimesliderToggleButton); +export { connected as TimesliderToggleButton }; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/timeslider_toggle_button.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/timeslider_toggle_button.tsx new file mode 100644 index 00000000000000..9332c2baaa5029 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/timeslider_toggle_button/timeslider_toggle_button.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiButtonIcon, EuiPanel } from '@elastic/eui'; +import { ClockPlayIcon } from './clock_play_icon'; + +export interface Props { + isTimesliderOpen: boolean; + openTimeslider: () => void; + closeTimeslider: () => void; +} + +export function TimesliderToggleButton(props: Props) { + function onClick() { + if (props.isTimesliderOpen) { + props.closeTimeslider(); + } else { + props.openTimeslider(); + } + } + + const label = props.isTimesliderOpen + ? i18n.translate('xpack.maps.timesliderToggleButton.closeLabel', { + defaultMessage: 'Close timeslider', + }) + : i18n.translate('xpack.maps.timesliderToggleButton.openLabel', { + defaultMessage: 'Open timeslider', + }); + + return ( + + + + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx index d8ac971ae3983a..28b5ab9c78f401 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.test.tsx @@ -19,12 +19,14 @@ jest.mock('../../kibana_services', () => { import { ToolbarOverlay } from './toolbar_overlay'; -test('Must render zoom tools', async () => { - const component = shallow(); +test('Should only show set view control', async () => { + const component = shallow( + + ); expect(component).toMatchSnapshot(); }); -test('Must zoom tools and draw filter tools', async () => { +test('Should show all controls', async () => { const geoFieldWithIndex = { geoFieldName: 'myGeoFieldName', geoFieldType: 'geo_point', @@ -35,6 +37,8 @@ test('Must zoom tools and draw filter tools', async () => { {}} geoFields={[geoFieldWithIndex]} + showFitToBoundsButton={true} + showTimesliderButton={true} /> ); expect(component).toMatchSnapshot(); diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx index c5208bc254fc89..41c6c1f7c4a7cd 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/toolbar_overlay.tsx @@ -12,6 +12,7 @@ import { ActionExecutionContext, Action } from 'src/plugins/ui_actions/public'; import { SetViewControl } from './set_view_control'; import { ToolsControl } from './tools_control'; import { FitToData } from './fit_to_data'; +import { TimesliderToggleButton } from './timeslider_toggle_button'; import { GeoFieldWithIndex } from '../../components/geo_field_with_index'; export interface Props { @@ -19,25 +20,33 @@ export interface Props { geoFields: GeoFieldWithIndex[]; getFilterActions?: () => Promise; getActionContext?: () => ActionExecutionContext; + showFitToBoundsButton: boolean; + showTimesliderButton: boolean; } export function ToolbarOverlay(props: Props) { - function renderToolsControl() { - const { addFilters, geoFields, getFilterActions, getActionContext } = props; - if (!addFilters || !geoFields.length) { - return null; - } - - return ( + const toolsButton = + props.addFilters && props.geoFields.length ? ( - ); - } + ) : null; + + const fitToBoundsButton = props.showFitToBoundsButton ? ( + + + + ) : null; + + const timesliderToogleButon = props.showTimesliderButton ? ( + + + + ) : null; return ( - - - + {fitToBoundsButton} + + {toolsButton} - {renderToolsControl()} + {timesliderToogleButon} ); } diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index 643199dbf3933d..f9eae1c90a1646 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import _ from 'lodash'; import React from 'react'; import { Provider } from 'react-redux'; @@ -27,6 +28,7 @@ import { Query, RefreshInterval, } from '../../../../../src/plugins/data/public'; +import { createExtentFilter } from '../../common/elasticsearch_util'; import { replaceLayerList, setMapSettings, @@ -43,8 +45,11 @@ import { EventHandlers, } from '../reducers/non_serializable_instances'; import { + getGeoFieldNames, getMapCenter, getMapBuffer, + getMapExtent, + getMapReady, getMapZoom, getHiddenLayerIds, getQueryableUniqueIndexPatternIds, @@ -64,7 +69,7 @@ import { getChartsPaletteServiceGetColor, getSearchService, } from '../kibana_services'; -import { LayerDescriptor } from '../../common/descriptor_types'; +import { LayerDescriptor, MapExtent } from '../../common/descriptor_types'; import { MapContainer } from '../connected_components/map_container'; import { SavedMap } from '../routes/map_page'; import { getIndexPatternsFromIds } from '../index_pattern_util'; @@ -96,16 +101,19 @@ export class MapEmbeddable private _savedMap: SavedMap; private _renderTooltipContent?: RenderToolTipContent; private _subscription: Subscription; + private _prevFilterByMapExtent: boolean; private _prevIsRestore: boolean = false; + private _prevMapExtent?: MapExtent; private _prevTimeRange?: TimeRange; private _prevQuery?: Query; private _prevRefreshConfig?: RefreshInterval; - private _prevFilters?: Filter[]; + private _prevFilters: Filter[] = []; private _prevSyncColors?: boolean; private _prevSearchSessionId?: string; private _domNode?: HTMLElement; private _unsubscribeFromStore?: Unsubscribe; private _isInitialized = false; + private _controlledBy: string; constructor(config: MapEmbeddableConfig, initialInput: MapEmbeddableInput, parent?: IContainer) { super( @@ -122,6 +130,9 @@ export class MapEmbeddable this._savedMap = new SavedMap({ mapEmbeddableInput: initialInput }); this._initializeSaveMap(); this._subscription = this.getUpdated$().subscribe(() => this.onUpdate()); + this._controlledBy = `mapEmbeddablePanel${this.id}`; + this._prevFilterByMapExtent = + this.input.filterByMapExtent === undefined ? false : this.input.filterByMapExtent; } private async _initializeSaveMap() { @@ -151,6 +162,11 @@ export class MapEmbeddable const store = this._savedMap.getStore(); store.dispatch(setReadOnly(true)); store.dispatch(disableScrollZoom()); + store.dispatch( + setMapSettings({ + showTimesliderToggleButton: false, + }) + ); this._dispatchSetQuery({ forceRefresh: false, @@ -221,11 +237,23 @@ export class MapEmbeddable } onUpdate() { + if ( + this.input.filterByMapExtent !== undefined && + this._prevFilterByMapExtent !== this.input.filterByMapExtent + ) { + this._prevFilterByMapExtent = this.input.filterByMapExtent; + if (this.input.filterByMapExtent) { + this.setMapExtentFilter(); + } else { + this.clearMapExtentFilter(); + } + } + if ( !_.isEqual(this.input.timeRange, this._prevTimeRange) || !_.isEqual(this.input.query, this._prevQuery) || - !esFilters.onlyDisabledFiltersChanged(this.input.filters, this._prevFilters) || - this.input.searchSessionId !== this._prevSearchSessionId + !esFilters.compareFilters(this._getFilters(), this._prevFilters) || + this._getSearchSessionId() !== this._prevSearchSessionId ) { this._dispatchSetQuery({ forceRefresh: false, @@ -240,7 +268,7 @@ export class MapEmbeddable this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors); } - const isRestore = getIsRestore(this.input.searchSessionId); + const isRestore = getIsRestore(this._getSearchSessionId()); if (isRestore !== this._prevIsRestore) { this._prevIsRestore = isRestore; this._savedMap.getStore().dispatch( @@ -252,22 +280,38 @@ export class MapEmbeddable } } + _getFilters() { + return this.input.filters + ? this.input.filters.filter( + (filter) => !filter.meta.disabled && filter.meta.controlledBy !== this._controlledBy + ) + : []; + } + + _getSearchSessionId() { + // New search session id causes all layers from elasticsearch to refetch data. + // Dashboard provides a new search session id anytime filters change. + // Thus, filtering embeddable container by map extent causes a new search session id any time the map is moved. + // Disabling search session when filtering embeddable container by map extent. + // The use case for search sessions (restoring results because of slow responses) does not match the use case of + // filtering by map extent (rapid responses as users explore their map). + return this.input.filterByMapExtent ? undefined : this.input.searchSessionId; + } + _dispatchSetQuery({ forceRefresh }: { forceRefresh: boolean }) { + const filters = this._getFilters(); this._prevTimeRange = this.input.timeRange; this._prevQuery = this.input.query; - this._prevFilters = this.input.filters; - this._prevSearchSessionId = this.input.searchSessionId; - const enabledFilters = this.input.filters - ? this.input.filters.filter((filter) => !filter.meta.disabled) - : []; + this._prevFilters = filters; + this._prevSearchSessionId = this._getSearchSessionId(); this._savedMap.getStore().dispatch( setQuery({ - filters: enabledFilters, + filters, query: this.input.query, timeFilters: this.input.timeRange, forceRefresh, - searchSessionId: this.input.searchSessionId, - searchSessionMapBuffer: getIsRestore(this.input.searchSessionId) + searchSessionId: this._getSearchSessionId(), + searchSessionMapBuffer: getIsRestore(this._getSearchSessionId()) ? this.input.mapBuffer : undefined, }) @@ -403,6 +447,57 @@ export class MapEmbeddable } as ActionExecutionContext; }; + setMapExtentFilter() { + const state = this._savedMap.getStore().getState(); + const mapExtent = getMapExtent(state); + const geoFieldNames = getGeoFieldNames(state); + const center = getMapCenter(state); + const zoom = getMapZoom(state); + + if (center === undefined || mapExtent === undefined || geoFieldNames.length === 0) { + return; + } + + this._prevMapExtent = mapExtent; + + const mapExtentFilter = createExtentFilter(mapExtent, geoFieldNames); + mapExtentFilter.meta.isMultiIndex = true; + mapExtentFilter.meta.controlledBy = this._controlledBy; + mapExtentFilter.meta.alias = i18n.translate('xpack.maps.embeddable.boundsFilterLabel', { + defaultMessage: 'Map bounds at center: {lat}, {lon}, zoom: {zoom}', + values: { + lat: center.lat, + lon: center.lon, + zoom, + }, + }); + + const executeContext = { + ...this.getActionContext(), + filters: [mapExtentFilter], + controlledBy: this._controlledBy, + }; + const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER); + if (!action) { + throw new Error('Unable to apply map extent filter, could not locate action'); + } + action.execute(executeContext); + } + + clearMapExtentFilter() { + this._prevMapExtent = undefined; + const executeContext = { + ...this.getActionContext(), + filters: [], + controlledBy: this._controlledBy, + }; + const action = getUiActions().getAction(ACTION_GLOBAL_APPLY_FILTER); + if (!action) { + throw new Error('Unable to apply map extent filter, could not locate action'); + } + action.execute(executeContext); + } + destroy() { super.destroy(); this._isActive = false; @@ -426,9 +521,15 @@ export class MapEmbeddable } _handleStoreChanges() { - if (!this._isActive) { + if (!this._isActive || !getMapReady(this._savedMap.getStore().getState())) { return; } + + const mapExtent = getMapExtent(this._savedMap.getStore().getState()); + if (this.input.filterByMapExtent && !_.isEqual(this._prevMapExtent, mapExtent)) { + this.setMapExtentFilter(); + } + const center = getMapCenter(this._savedMap.getStore().getState()); const zoom = getMapZoom(this._savedMap.getStore().getState()); diff --git a/x-pack/plugins/maps/public/embeddable/types.ts b/x-pack/plugins/maps/public/embeddable/types.ts index 7cd4fa8e1253bd..79a70f3786fe6c 100644 --- a/x-pack/plugins/maps/public/embeddable/types.ts +++ b/x-pack/plugins/maps/public/embeddable/types.ts @@ -35,9 +35,10 @@ interface MapEmbeddableState { } export type MapByValueInput = { attributes: MapSavedObjectAttributes; -} & EmbeddableInput & - MapEmbeddableState; -export type MapByReferenceInput = SavedObjectEmbeddableInput & MapEmbeddableState; +} & EmbeddableInput & { filterByMapExtent?: boolean } & MapEmbeddableState; +export type MapByReferenceInput = SavedObjectEmbeddableInput & { + filterByMapExtent?: boolean; +} & MapEmbeddableState; export type MapEmbeddableInput = MapByValueInput | MapByReferenceInput; export type MapEmbeddableOutput = EmbeddableOutput & { diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index e4b9397fab8e79..1652e78d3d2cbf 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -22,6 +22,9 @@ export function setStartServices(core: CoreStart, plugins: MapsPluginStartDepend coreStart = core; pluginsStart = plugins; } + +export const getIndexNameFormComponent = () => pluginsStart.fileUpload.IndexNameFormComponent; +export const getFileUploadComponent = () => pluginsStart.fileUpload.FileUploadComponent; export const getIndexPatternService = () => pluginsStart.data.indexPatterns; export const getAutocompleteService = () => pluginsStart.data.autocomplete; export const getInspector = () => pluginsStart.inspector; @@ -55,7 +58,6 @@ let mapAppConfig: MapsConfigType; export const setMapAppConfig = (config: MapsConfigType) => (mapAppConfig = config); export const getMapAppConfig = () => mapAppConfig; -export const getEnabled = () => getMapAppConfig().enabled; export const getShowMapsInspectorAdapter = () => getMapAppConfig().showMapsInspectorAdapter; export const getPreserveDrawingBuffer = () => getMapAppConfig().preserveDrawingBuffer; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index ad8846bd48b608..740112124a2510 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -42,8 +42,10 @@ import { createTileMapUrlGenerator, } from './url_generator'; import { visualizeGeoFieldAction } from './trigger_actions/visualize_geo_field_action'; +import { filterByMapExtentAction } from './trigger_actions/filter_by_map_extent_action'; import { MapEmbeddableFactory } from './embeddable/map_embeddable_factory'; import type { EmbeddableSetup, EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { MapsXPackConfig, MapsConfigType } from '../config'; import { getAppTitle } from '../common/i18n_getters'; import { lazyLoadMapModules } from './lazy_load_bundle'; @@ -173,6 +175,7 @@ export class MapsPlugin if (core.application.capabilities.maps.show) { plugins.uiActions.addTriggerAction(VISUALIZE_GEO_FIELD_TRIGGER, visualizeGeoFieldAction); } + plugins.uiActions.addTriggerAction(CONTEXT_MENU_TRIGGER, filterByMapExtentAction); if (!core.application.capabilities.maps.save) { plugins.visualizations.unRegisterAlias(APP_ID); diff --git a/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts index 8ecaa8dfc2bf5e..c73bf46d4bc0c9 100644 --- a/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts +++ b/x-pack/plugins/maps/public/reducers/map/default_map_settings.ts @@ -25,6 +25,7 @@ export function getDefaultMapSettings(): MapSettings { minZoom: MIN_ZOOM, showScaleControl: false, showSpatialFilters: true, + showTimesliderToggleButton: true, spatialFiltersAlpa: 0.3, spatialFiltersFillColor: '#DA8B45', spatialFiltersLineColor: '#DA8B45', diff --git a/x-pack/plugins/maps/public/reducers/map/map.ts b/x-pack/plugins/maps/public/reducers/map/map.ts index daeba8e9982b07..591215efcddae5 100644 --- a/x-pack/plugins/maps/public/reducers/map/map.ts +++ b/x-pack/plugins/maps/public/reducers/map/map.ts @@ -73,6 +73,7 @@ export const DEFAULT_MAP_STATE: MapState = { extent: undefined, mouseCoordinates: undefined, timeFilters: undefined, + timeslice: undefined, query: undefined, filters: [], refreshConfig: undefined, @@ -218,13 +219,21 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: any) { }; return { ...state, mapState: { ...state.mapState, ...newMapState } }; case SET_QUERY: - const { query, timeFilters, filters, searchSessionId, searchSessionMapBuffer } = action; + const { + query, + timeFilters, + timeslice, + filters, + searchSessionId, + searchSessionMapBuffer, + } = action; return { ...state, mapState: { ...state.mapState, query, timeFilters, + timeslice, filters, searchSessionId, searchSessionMapBuffer, diff --git a/x-pack/plugins/maps/public/reducers/map/types.ts b/x-pack/plugins/maps/public/reducers/map/types.ts index 6b10b4a66fb611..e8dd7261e3dd34 100644 --- a/x-pack/plugins/maps/public/reducers/map/types.ts +++ b/x-pack/plugins/maps/public/reducers/map/types.ts @@ -15,6 +15,7 @@ import { MapExtent, MapQuery, MapRefreshConfig, + Timeslice, TooltipState, } from '../../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../../common/constants'; @@ -31,6 +32,7 @@ export type MapContext = { lon: number; }; timeFilters?: TimeRange; + timeslice?: Timeslice; query?: MapQuery; filters: Filter[]; refreshConfig?: MapRefreshConfig; @@ -61,6 +63,7 @@ export type MapSettings = { minZoom: number; showScaleControl: boolean; showSpatialFilters: boolean; + showTimesliderToggleButton: boolean; spatialFiltersAlpa: number; spatialFiltersFillColor: string; spatialFiltersLineColor: string; diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index 676ac6ce12efeb..9f948ce3d64739 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -12,6 +12,7 @@ import { getMapsCapabilities } from '../kibana_services'; import { UPDATE_FLYOUT, SET_IS_LAYER_TOC_OPEN, + SET_IS_TIME_SLIDER_OPEN, SET_FULL_SCREEN, SET_READ_ONLY, SET_OPEN_TOC_DETAILS, @@ -31,6 +32,7 @@ export type MapUiState = { isFullScreen: boolean; isReadOnly: boolean; isLayerTOCOpen: boolean; + isTimesliderOpen: boolean; openTOCDetails: string[]; }; @@ -41,6 +43,7 @@ export const DEFAULT_MAP_UI_STATE = { isFullScreen: false, isReadOnly: !getMapsCapabilities().save, isLayerTOCOpen: DEFAULT_IS_LAYER_TOC_OPEN, + isTimesliderOpen: false, // storing TOC detail visibility outside of map.layerList because its UI state and not map rendering state. // This also makes for easy read/write access for embeddables. openTOCDetails: [], @@ -53,6 +56,8 @@ export function ui(state: MapUiState = DEFAULT_MAP_UI_STATE, action: any) { return { ...state, flyoutDisplay: action.display }; case SET_IS_LAYER_TOC_OPEN: return { ...state, isLayerTOCOpen: action.isLayerTOCOpen }; + case SET_IS_TIME_SLIDER_OPEN: + return { ...state, isTimesliderOpen: action.isTimesliderOpen }; case SET_FULL_SCREEN: return { ...state, isFullScreen: action.isFullScreen }; case SET_READ_ONLY: diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 6a31e25197210d..21b50aba440f5a 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -42,7 +42,7 @@ import { getTopNavConfig } from '../top_nav_config'; import { MapRefreshConfig, MapQuery } from '../../../../common/descriptor_types'; import { goToSpecifiedPath } from '../../../render_app'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { getExistingMapPath } from '../../../../common/constants'; +import { getExistingMapPath, APP_ID } from '../../../../common/constants'; import { getInitialQuery, getInitialRefreshConfig, @@ -355,7 +355,7 @@ export class MapApp extends React.Component { return ( { }; const mapZoom = 4; const timeFilters = { to: '2001-01-01', from: '2001-12-31' }; + const timeslice = undefined; const refreshTimerLastTriggeredAt = '2001-01-01T00:00:00'; const query = undefined; const filters: Filter[] = []; @@ -74,6 +75,7 @@ describe('getDataFilters', () => { mapBuffer, mapZoom, timeFilters, + timeslice, refreshTimerLastTriggeredAt, query, filters, @@ -89,6 +91,7 @@ describe('getDataFilters', () => { mapBuffer, mapZoom, timeFilters, + timeslice, refreshTimerLastTriggeredAt, query, filters, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index a818cdd2d00f92..5be14737cff15c 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -176,6 +176,8 @@ export const getMouseCoordinates = ({ map }: MapStoreState) => map.mapState.mous export const getTimeFilters = ({ map }: MapStoreState): TimeRange => map.mapState.timeFilters ? map.mapState.timeFilters : getTimeFilter().getTime(); +export const getTimeslice = ({ map }: MapStoreState) => map.mapState.timeslice; + export const getQuery = ({ map }: MapStoreState): MapQuery | undefined => map.mapState.query; export const getFilters = ({ map }: MapStoreState): Filter[] => map.mapState.filters; @@ -234,6 +236,7 @@ export const getDataFilters = createSelector( getMapBuffer, getMapZoom, getTimeFilters, + getTimeslice, getRefreshTimerLastTriggeredAt, getQuery, getFilters, @@ -244,6 +247,7 @@ export const getDataFilters = createSelector( mapBuffer, mapZoom, timeFilters, + timeslice, refreshTimerLastTriggeredAt, query, filters, @@ -255,6 +259,7 @@ export const getDataFilters = createSelector( buffer: searchSessionId && searchSessionMapBuffer ? searchSessionMapBuffer : mapBuffer, zoom: mapZoom, timeFilters, + timeslice, refreshTimerLastTriggeredAt, query, filters, @@ -401,6 +406,26 @@ export const getQueryableUniqueIndexPatternIds = createSelector( } ); +export const getGeoFieldNames = createSelector( + getLayerList, + getWaitingForMapReadyLayerListRaw, + (layerList, waitingForMapReadyLayerList) => { + const geoFieldNames: string[] = []; + + if (waitingForMapReadyLayerList.length) { + waitingForMapReadyLayerList.forEach((layerDescriptor) => { + const layer = createLayerInstance(layerDescriptor); + geoFieldNames.push(...layer.getGeoFieldNames()); + }); + } else { + layerList.forEach((layer) => { + geoFieldNames.push(...layer.getGeoFieldNames()); + }); + } + return _.uniq(geoFieldNames); + } +); + export const hasDirtyState = createSelector(getLayerListRaw, (layerListRaw) => { return layerListRaw.some((layerDescriptor) => { if (layerDescriptor.__isPreviewLayer) { diff --git a/x-pack/plugins/maps/public/selectors/ui_selectors.ts b/x-pack/plugins/maps/public/selectors/ui_selectors.ts index e5c83bd0f8f4ab..c9ec8bac6d83ae 100644 --- a/x-pack/plugins/maps/public/selectors/ui_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/ui_selectors.ts @@ -11,6 +11,7 @@ import { FLYOUT_STATE } from '../reducers/ui'; export const getFlyoutDisplay = ({ ui }: MapStoreState): FLYOUT_STATE => ui.flyoutDisplay; export const getIsLayerTOCOpen = ({ ui }: MapStoreState): boolean => ui.isLayerTOCOpen; +export const getIsTimesliderOpen = ({ ui }: MapStoreState): boolean => ui.isTimesliderOpen; export const getOpenTOCDetails = ({ ui }: MapStoreState): string[] => ui.openTOCDetails; export const getIsFullScreen = ({ ui }: MapStoreState): boolean => ui.isFullScreen; export const getIsReadOnly = ({ ui }: MapStoreState): boolean => ui.isReadOnly; diff --git a/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.ts b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.ts new file mode 100644 index 00000000000000..7706704cdd63d6 --- /dev/null +++ b/x-pack/plugins/maps/public/trigger_actions/filter_by_map_extent_action.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { + Embeddable, + EmbeddableInput, + ViewMode, +} from '../../../../../src/plugins/embeddable/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; +import { createAction } from '../../../../../src/plugins/ui_actions/public'; + +export const FILTER_BY_MAP_EXTENT = 'FILTER_BY_MAP_EXTENT'; + +interface FilterByMapExtentInput extends EmbeddableInput { + filterByMapExtent: boolean; +} + +interface FilterByMapExtentActionContext { + embeddable: Embeddable; +} + +export const filterByMapExtentAction = createAction({ + id: FILTER_BY_MAP_EXTENT, + type: FILTER_BY_MAP_EXTENT, + order: 20, + getDisplayName: ({ embeddable }: FilterByMapExtentActionContext) => { + return embeddable.getInput().filterByMapExtent + ? i18n.translate('xpack.maps.filterByMapExtentMenuItem.disableDisplayName', { + defaultMessage: 'Disable filter by map extent', + }) + : i18n.translate('xpack.maps.filterByMapExtentMenuItem.enableDisplayName', { + defaultMessage: 'Enable filter by map extent', + }); + }, + getIconType: () => { + return 'filter'; + }, + isCompatible: async ({ embeddable }: FilterByMapExtentActionContext) => { + return ( + embeddable.type === MAP_SAVED_OBJECT_TYPE && embeddable.getInput().viewMode === ViewMode.EDIT + ); + }, + execute: async ({ embeddable }: FilterByMapExtentActionContext) => { + embeddable.updateInput({ + filterByMapExtent: !embeddable.getInput().filterByMapExtent, + }); + }, +}); diff --git a/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts b/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts index 2b8984aa1534a1..22c3da61244afa 100644 --- a/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts +++ b/x-pack/plugins/maps/server/data_indexing/create_doc_source.ts @@ -29,9 +29,10 @@ export async function createDocSource( ): Promise { try { await createIndex(index, mappings, asCurrentUser); - await indexPatternsService.createAndSave({ title: index }, true); + const { id: indexPatternId } = await indexPatternsService.createAndSave({ title: index }, true); return { + indexPatternId, success: true, }; } catch (error) { diff --git a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts index e6e6471ff9af61..951d7ed085f39b 100644 --- a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts +++ b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts @@ -11,8 +11,8 @@ import { IRouter } from 'src/core/server'; import type { DataRequestHandlerContext } from 'src/plugins/data/server'; import { INDEX_SOURCE_API_PATH, - GIS_API_PATH, MAX_DRAWING_SIZE_BYTES, + INDEX_FEATURE_PATH, } from '../../common/constants'; import { createDocSource } from './create_doc_source'; import { writeDataToIndex } from './index_data'; @@ -70,7 +70,7 @@ export function initIndexingRoutes({ router.post( { - path: `/${GIS_API_PATH}/feature`, + path: INDEX_FEATURE_PATH, validate: { body: schema.object({ index: schema.string(), diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts index f77e7d182a3bba..fecad3ae5cf97d 100644 --- a/x-pack/plugins/maps/server/index.ts +++ b/x-pack/plugins/maps/server/index.ts @@ -16,6 +16,7 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { enabled: true, showMapVisualizationTypes: true, + enableDrawingFeature: true, showMapsInspectorAdapter: true, preserveDrawingBuffer: true, }, diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 1e6a76caf70e9a..3545a85305c178 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -117,8 +117,12 @@ export function getPluginPrivileges() { read: savedObjects, }, alerting: { - all: Object.values(ML_ALERT_TYPES), - read: [], + rule: { + all: Object.values(ML_ALERT_TYPES), + }, + alert: { + all: Object.values(ML_ALERT_TYPES), + }, }, }, user: { @@ -132,8 +136,12 @@ export function getPluginPrivileges() { read: savedObjects, }, alerting: { - all: [], - read: Object.values(ML_ALERT_TYPES), + rule: { + read: Object.values(ML_ALERT_TYPES), + }, + alert: { + read: Object.values(ML_ALERT_TYPES), + }, }, }, apmUser: { diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap index 7d5c73b42f15bb..d14fc6df066933 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap @@ -18,6 +18,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe /> = ({ - + @@ -239,6 +239,7 @@ export const CustomUrlEditor: FC = ({ idSelected={type} onChange={onTypeChange} className="url-link-to-radio" + data-test-subj="mlJobCustomUrlLinkToTypeInput" /> @@ -256,6 +257,7 @@ export const CustomUrlEditor: FC = ({ options={dashboardOptions} value={kibanaSettings.dashboardId} onChange={onDashboardChange} + data-test-subj="mlJobCustomUrlDashboardNameInput" compressed /> @@ -275,6 +277,7 @@ export const CustomUrlEditor: FC = ({ options={indexPatternOptions} value={kibanaSettings.discoverIndexPatternId} onChange={onDiscoverIndexPatternChange} + data-test-subj="mlJobCustomUrlDiscoverIndexPatternInput" compressed /> @@ -298,6 +301,7 @@ export const CustomUrlEditor: FC = ({ selectedOptions={selectedEntityOptions} onChange={onQueryEntitiesChange} isClearable={true} + data-test-subj="mlJobCustomUrlQueryEntitiesInput" /> )} @@ -321,6 +325,7 @@ export const CustomUrlEditor: FC = ({ options={timeRangeOptions} value={timeRange.type} onChange={onTimeRangeTypeChange} + data-test-subj="mlJobCustomUrlTimeRangeInput" compressed /> @@ -343,6 +348,7 @@ export const CustomUrlEditor: FC = ({ value={timeRange.interval} onChange={onTimeRangeIntervalChange} isInvalid={isInvalidTimeRange} + data-test-subj="mlJobCustomUrlTimeRangeIntervalInput" compressed /> @@ -365,6 +371,7 @@ export const CustomUrlEditor: FC = ({ rows={2} value={otherUrlSettings.urlValue} onChange={onOtherUrlValueChange} + data-test-subj="mlJobCustomUrlOtherTypeUrlInput" compressed /> diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx index 5c61de38df37bf..96b09aff64f0c9 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx @@ -160,6 +160,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust } isInvalid={isInvalidLabel} error={invalidLabelError} + data-test-subj="mlJobEditCustomUrlItemLabel" > = ({ job, customUrls, setCust aria-label={i18n.translate('xpack.ml.customUrlEditorList.testCustomUrlAriaLabel', { defaultMessage: 'Test custom URL', })} + data-test-subj="mlJobEditTestCustomUrlButton" /> @@ -264,6 +266,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust defaultMessage: 'Delete custom URL', } )} + data-test-subj={`mlJobEditDeleteCustomUrlButton_${index}`} /> diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 758e3fa472a0b8..d7f42daf5f3c13 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -326,6 +326,7 @@ export class EditJobFlyoutUI extends Component { const tabs = [ { id: 'job-details', + 'data-test-subj': 'mlEditJobFlyout-jobDetails', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.jobDetailsTitle', { defaultMessage: 'Job details', }), @@ -346,6 +347,7 @@ export class EditJobFlyoutUI extends Component { }, { id: 'detectors', + 'data-test-subj': 'mlEditJobFlyout-detectors', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.detectorsTitle', { defaultMessage: 'Detectors', }), @@ -359,6 +361,7 @@ export class EditJobFlyoutUI extends Component { }, { id: 'datafeed', + 'data-test-subj': 'mlEditJobFlyout-datafeed', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.datafeedTitle', { defaultMessage: 'Datafeed', }), @@ -376,6 +379,7 @@ export class EditJobFlyoutUI extends Component { }, { id: 'custom-urls', + 'data-test-subj': 'mlEditJobFlyout-customUrls', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.customUrlsTitle', { defaultMessage: 'Custom URLs', }), @@ -395,6 +399,7 @@ export class EditJobFlyoutUI extends Component { this.closeFlyout(); }} size="m" + data-test-subj="mlJobEditFlyout" > @@ -419,6 +424,7 @@ export class EditJobFlyoutUI extends Component { this.closeFlyout(); }} flush="left" + data-test-subj="mlEditJobFlyoutCloseButton" > { uiSettings: { get: () => false }, http: { basePath: { prepend: (path: string) => path } }, } as unknown) as CoreStart; - const config = { unsafe: { alertingExperience: { enabled: true } } }; + const config = { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }; const params = ({ element: window.document.createElement('div'), history: createMemoryHistory(), @@ -59,6 +60,7 @@ describe('renderApp', () => { plugins, appMountParameters: params, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: KibanaPageTemplate, }); unmount(); }).not.toThrowError(); diff --git a/x-pack/plugins/observability/public/application/index.tsx b/x-pack/plugins/observability/public/application/index.tsx index 460aa6c35bdb8e..f8dce3ce1d487e 100644 --- a/x-pack/plugins/observability/public/application/index.tsx +++ b/x-pack/plugins/observability/public/application/index.tsx @@ -10,7 +10,7 @@ import React, { MouseEvent, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Route, Router, Switch } from 'react-router-dom'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; -import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; +import { AppMountParameters, APP_WRAPPER_CLASS, CoreStart } from '../../../../../src/core/public'; import { KibanaContextProvider, RedirectAppLinks, @@ -19,6 +19,7 @@ import { PluginContext } from '../context/plugin_context'; import { usePluginContext } from '../hooks/use_plugin_context'; import { useRouteParams } from '../hooks/use_route_params'; import { ObservabilityPublicPluginsStart } from '../plugin'; +import type { LazyObservabilityPageTemplateProps } from '../components/shared/page_template/lazy_page_template'; import { HasDataContextProvider } from '../context/has_data_context'; import { Breadcrumbs, routes } from '../routes'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; @@ -74,12 +75,14 @@ export const renderApp = ({ plugins, appMountParameters, observabilityRuleTypeRegistry, + ObservabilityPageTemplate, }: { config: ConfigSchema; core: CoreStart; plugins: ObservabilityPublicPluginsStart; observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; appMountParameters: AppMountParameters; + ObservabilityPageTemplate: React.ComponentType; }) => { const { element, history } = appMountParameters; const i18nCore = core.i18n; @@ -92,15 +95,25 @@ export const renderApp = ({ links: [{ linkType: 'discuss', href: 'https://ela.st/observability-discuss' }], }); + // ensure all divs are .kbnAppWrappers + element.classList.add(APP_WRAPPER_CLASS); + ReactDOM.render( - + diff --git a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx new file mode 100644 index 00000000000000..707cb241501fdc --- /dev/null +++ b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink, EuiHeaderLinks } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { usePluginContext } from '../../../hooks/use_plugin_context'; +import HeaderMenuPortal from '../../shared/header_menu_portal'; + +export function ObservabilityHeaderMenu(): React.ReactElement | null { + const { + appMountParameters: { setHeaderActionMenu }, + core: { + http: { + basePath: { prepend }, + }, + }, + } = usePluginContext(); + + return ( + + + + {addDataLinkText} + + + + ); +} + +const addDataLinkText = i18n.translate('xpack.observability.home.addData', { + defaultMessage: 'Add data', +}); diff --git a/x-pack/plugins/observability/public/components/app/header/index.test.tsx b/x-pack/plugins/observability/public/components/app/header/index.test.tsx deleted file mode 100644 index 65724dd74598de..00000000000000 --- a/x-pack/plugins/observability/public/components/app/header/index.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '../../../utils/test_helper'; -import { Header } from './'; - -describe('Header', () => { - it('renders', () => { - const { getByText, getByTestId } = render(
); - expect(getByTestId('observability-logo')).toBeInTheDocument(); - expect(getByText('Observability')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index 8b86e0b25379b8..f7f69c22173fc5 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -5,81 +5,4 @@ * 2.0. */ -import { - EuiFlexGroup, - EuiFlexItem, - EuiHeaderLink, - EuiHeaderLinks, - EuiIcon, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { ReactNode } from 'react'; -import styled from 'styled-components'; -import { usePluginContext } from '../../../hooks/use_plugin_context'; -import HeaderMenuPortal from '../../shared/header_menu_portal'; - -const Container = styled.div<{ color: string }>` - background: ${(props) => props.color}; - border-bottom: ${(props) => props.theme.eui.euiBorderThin}; -`; - -const Wrapper = styled.div<{ restrictWidth?: number }>` - width: 100%; - max-width: ${(props) => `${props.restrictWidth}px`}; - margin: 0 auto; - overflow: hidden; - padding: 0 16px; -`; - -interface Props { - color: string; - datePicker?: ReactNode; - restrictWidth?: number; -} - -export function Header({ color, datePicker = null, restrictWidth }: Props) { - const { appMountParameters, core } = usePluginContext(); - const { setHeaderActionMenu } = appMountParameters; - const { prepend } = core.http.basePath; - - return ( - - - - - {i18n.translate('xpack.observability.home.addData', { defaultMessage: 'Add data' })} - - - - - - - - - - - - - -

- {i18n.translate('xpack.observability.home.title', { - defaultMessage: 'Observability', - })} -

-
-
-
-
- {datePicker} -
- -
-
- ); -} +export * from './header_menu'; diff --git a/x-pack/plugins/observability/public/components/app/layout/with_header.tsx b/x-pack/plugins/observability/public/components/app/layout/with_header.tsx deleted file mode 100644 index f2d50539395bdf..00000000000000 --- a/x-pack/plugins/observability/public/components/app/layout/with_header.tsx +++ /dev/null @@ -1,48 +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 { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui'; -import React, { ReactNode } from 'react'; -import styled from 'styled-components'; -import { Header } from '../header/index'; - -const Page = styled(EuiPage)` - background: transparent; -`; - -const Container = styled.div<{ color?: string }>` - overflow-y: hidden; - min-height: calc( - 100vh - ${(props) => props.theme.eui.euiHeaderHeight + props.theme.eui.euiHeaderHeight} - ); - background: ${(props) => props.color}; -`; - -interface Props { - datePicker?: ReactNode; - headerColor: string; - bodyColor: string; - children?: ReactNode; - restrictWidth?: number; -} - -export function WithHeaderLayout({ - datePicker, - headerColor, - bodyColor, - children, - restrictWidth, -}: Props) { - return ( - -
- - {children} - - - ); -} diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx index 67fede05f3ced6..ad3ecd27408029 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.test.tsx @@ -16,6 +16,7 @@ import { HasDataContextValue } from '../../../../context/has_data_context'; import { AppMountParameters, CoreStart } from 'kibana/public'; import { ObservabilityPublicPluginsStart } from '../../../../plugin'; import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; +import { KibanaPageTemplate } from '../../../../../../../../src/plugins/kibana_react/public'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -41,7 +42,7 @@ describe('APMSection', () => { http: { basePath: { prepend: jest.fn() } }, } as unknown) as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true } } }, + config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), plugins: ({ data: { @@ -57,6 +58,7 @@ describe('APMSection', () => { }, }, } as unknown) as ObservabilityPublicPluginsStart, + ObservabilityPageTemplate: KibanaPageTemplate, })); }); diff --git a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx index b4227cc122dde3..fab461476e7136 100644 --- a/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/section/ux/index.test.tsx @@ -16,6 +16,7 @@ import { render } from '../../../../utils/test_helper'; import { UXSection } from './'; import { response } from './mock_data/ux.mock'; import { createObservabilityRuleTypeRegistryMock } from '../../../../rules/observability_rule_type_registry_mock'; +import { KibanaPageTemplate } from '../../../../../../../../src/plugins/kibana_react/public'; jest.mock('react-router-dom', () => ({ useLocation: () => ({ @@ -40,7 +41,7 @@ describe('UXSection', () => { http: { basePath: { prepend: jest.fn() } }, } as unknown) as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true } } }, + config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, plugins: ({ data: { query: { @@ -56,6 +57,7 @@ describe('UXSection', () => { }, } as unknown) as ObservabilityPublicPluginsStart, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: KibanaPageTemplate, })); }); it('renders with core web vitals', () => { diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index d4d7521a6b0964..f04ca5b8579163 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -9,6 +9,8 @@ import React, { lazy, Suspense } from 'react'; import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; +export { createLazyObservabilityPageTemplate } from './page_template'; + const CoreVitalsLazy = lazy(() => import('./core_web_vitals/index')); export function getCoreVitalsComponent(props: CoreVitalProps) { diff --git a/x-pack/plugins/observability/public/components/shared/page_template/README.md b/x-pack/plugins/observability/public/components/shared/page_template/README.md new file mode 100644 index 00000000000000..e360e6d95a9d8a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/page_template/README.md @@ -0,0 +1,104 @@ +## Overview + +Observability solutions can register their navigation structures via the Observability plugin, this ensures that these navigation options display in the Observability page template component. This is a two part process, A) register your navigation structure and B) consume and render the shared page template component. These two elements are documented below. + +## Navigation registration + +To register a solution's navigation structure you'll first need to ensure your solution has the observability plugin specified as a dependency in your `kibana.json` file, e.g. + +```json +"requiredPlugins": [ + "observability" +], +``` + +Now within your solution's **public** plugin `setup` lifecycle method you can call the `registerSections` method, this will register your solution's specific navigation structure with the overall Observability navigation registry. E.g. + +```typescript +// x-pack/plugins/example_plugin/public/plugin.ts + +export class Plugin implements PluginClass { + constructor(_context: PluginInitializerContext) {} + + setup(core: CoreSetup, plugins: PluginsSetup) { + plugins.observability.navigation.registerSections( + of([ + { + label: 'A solution section', + sortKey: 200, + entries: [ + { label: 'Example Page', app: 'exampleA', path: '/example' }, + { label: 'Another Example Page', app: 'exampleA', path: '/another-example' }, + ], + }, + { + label: 'Another solution section', + sortKey: 300, + entries: [ + { label: 'Example page', app: 'exampleB', path: '/example' }, + ], + }, + ]) + ); + } + + start() {} + + stop() {} +} +``` + +Here `app` would match your solution - e.g. logs, metrics, APM, uptime etc. The registry is fully typed so please refer to the types for specific options. + +Observables are used to facilitate changes over time, for example within the lifetime of your application a license type or set of user permissions may change and as such you may wish to change the navigation structure. If your navigation needs are simple you can pass a value and forget about it. **Solutions are expected to handle their own permissions, and what should or should not be displayed at any time**, the Observability plugin will not add and remove items for you. + +The Observability navigation registry is now aware of your solution's navigation needs ✅ + +## Page template component + +The shared page template component can be used to actually display and render all of the registered navigation structures within your solution. + +The `start` contract of the public Observability plugin exposes a React component, under `navigation.PageTemplate`. + +This can be accessed like so: + +``` +const [coreStart, pluginsStart] = await core.getStartServices(); +const pageTemplateComponent = pluginsStart.observability.navigation.PageTemplate; +``` + +Now that you have access to the component you can render your solution's content using it. + +```jsx + , + ], + }} + > + // Render anything you like here, this is just an example. + + + // Content + + + + // Content + + + +``` + +The `` component is a wrapper around the `` component (which in turn is a wrapper around the `` component). As such the props mostly reflect those available on the wrapped components, again everything is fully typed so please refer to the types for specific options. The `pageSideBar` prop is handled by the component, and will take care of rendering out and managing the items from the registry. + +After these two steps we should see something like the following (note the navigation on the left): + +![Page template rendered example](./page_template.png) \ No newline at end of file diff --git a/x-pack/plugins/beats_management/common/constants/configuration_blocks.ts b/x-pack/plugins/observability/public/components/shared/page_template/index.ts similarity index 76% rename from x-pack/plugins/beats_management/common/constants/configuration_blocks.ts rename to x-pack/plugins/observability/public/components/shared/page_template/index.ts index 7f0a1442a374ef..14793ba4022515 100644 --- a/x-pack/plugins/beats_management/common/constants/configuration_blocks.ts +++ b/x-pack/plugins/observability/public/components/shared/page_template/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const UNIQUENESS_ENFORCING_TYPES = ['output']; +export { createLazyObservabilityPageTemplate } from './lazy_page_template'; diff --git a/x-pack/plugins/observability/public/components/shared/page_template/lazy_page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/lazy_page_template.tsx new file mode 100644 index 00000000000000..7c61cae4f2c73c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/page_template/lazy_page_template.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { + ObservabilityPageTemplateDependencies, + WrappedPageTemplateProps, +} from './page_template'; + +export const LazyObservabilityPageTemplate = React.lazy(() => import('./page_template')); + +export type LazyObservabilityPageTemplateProps = WrappedPageTemplateProps; + +export function createLazyObservabilityPageTemplate( + injectedDeps: ObservabilityPageTemplateDependencies +) { + return (pageTemplateProps: LazyObservabilityPageTemplateProps) => ( + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.png b/x-pack/plugins/observability/public/components/shared/page_template/page_template.png new file mode 100644 index 00000000000000..7dc88b937c27bf Binary files /dev/null and b/x-pack/plugins/observability/public/components/shared/page_template/page_template.png differ diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.test.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.test.tsx new file mode 100644 index 00000000000000..42d520786afcfa --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { I18nProvider } from '@kbn/i18n/react'; +import { render } from '@testing-library/react'; +import { shallow } from 'enzyme'; +import React from 'react'; +import { of } from 'rxjs'; +import { createNavigationRegistry } from '../../../services/navigation_registry'; +import { createLazyObservabilityPageTemplate } from './lazy_page_template'; +import { ObservabilityPageTemplate } from './page_template'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: () => ({ + pathname: '/test-path', + }), +})); + +const navigationRegistry = createNavigationRegistry(); + +navigationRegistry.registerSections( + of([ + { + label: 'Test A', + sortKey: 100, + entries: [ + { label: 'Section A Url A', app: 'TestA', path: '/url-a' }, + { label: 'Section A Url B', app: 'TestA', path: '/url-b' }, + ], + }, + { + label: 'Test B', + sortKey: 200, + entries: [ + { label: 'Section B Url A', app: 'TestB', path: '/url-a' }, + { label: 'Section B Url B', app: 'TestB', path: '/url-b' }, + ], + }, + ]) +); + +describe('Page template', () => { + it('Provides a working lazy wrapper', () => { + const LazyObservabilityPageTemplate = createLazyObservabilityPageTemplate({ + currentAppId$: of('Test app ID'), + getUrlForApp: () => '/test-url', + navigateToApp: async () => {}, + navigationSections$: navigationRegistry.sections$, + }); + + const component = shallow( + Test side item], + }} + > +
Test structure
+
+ ); + + expect(component.exists('lazy')).toBe(true); + }); + + it('Utilises the KibanaPageTemplate for rendering', () => { + const component = shallow( + '/test-url'} + navigateToApp={async () => {}} + navigationSections$={navigationRegistry.sections$} + pageHeader={{ + pageTitle: 'Test title', + rightSideItems: [Test side item], + }} + > +
Test structure
+
+ ); + + expect(component.is('KibanaPageTemplate')); + }); + + it('Handles outputting the registered navigation structures within a side nav', () => { + const { container } = render( + + '/test-url'} + navigateToApp={async () => {}} + navigationSections$={navigationRegistry.sections$} + pageHeader={{ + pageTitle: 'Test title', + rightSideItems: [Test side item], + }} + > +
Test structure
+
+
+ ); + + expect(container).toHaveTextContent('Section A Url A'); + expect(container).toHaveTextContent('Section A Url B'); + expect(container).toHaveTextContent('Section B Url A'); + expect(container).toHaveTextContent('Section B Url B'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx new file mode 100644 index 00000000000000..8025c6d658692f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSideNavItemType, ExclusiveUnion } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { useMemo } from 'react'; +import { matchPath, useLocation } from 'react-router-dom'; +import useObservable from 'react-use/lib/useObservable'; +import type { Observable } from 'rxjs'; +import type { ApplicationStart } from '../../../../../../../src/core/public'; +import { + KibanaPageTemplate, + KibanaPageTemplateProps, +} from '../../../../../../../src/plugins/kibana_react/public'; +import type { NavigationSection } from '../../../services/navigation_registry'; + +export type WrappedPageTemplateProps = Pick< + KibanaPageTemplateProps, + | 'children' + | 'data-test-subj' + | 'paddingSize' + | 'pageBodyProps' + | 'pageContentBodyProps' + | 'pageContentProps' + | 'pageHeader' + | 'restrictWidth' +> & + // recreate the exclusivity of bottomBar-related props + ExclusiveUnion< + { template?: 'default' } & Pick, + { template: KibanaPageTemplateProps['template'] } + >; + +export interface ObservabilityPageTemplateDependencies { + currentAppId$: Observable; + getUrlForApp: ApplicationStart['getUrlForApp']; + navigateToApp: ApplicationStart['navigateToApp']; + navigationSections$: Observable; +} + +export type ObservabilityPageTemplateProps = ObservabilityPageTemplateDependencies & + WrappedPageTemplateProps; + +export function ObservabilityPageTemplate({ + children, + currentAppId$, + getUrlForApp, + navigateToApp, + navigationSections$, + ...pageTemplateProps +}: ObservabilityPageTemplateProps): React.ReactElement | null { + const sections = useObservable(navigationSections$, []); + const currentAppId = useObservable(currentAppId$, undefined); + const { pathname: currentPath } = useLocation(); + + const sideNavItems = useMemo>>( + () => + sections.map(({ label, entries }, sectionIndex) => ({ + id: `${sectionIndex}`, + name: label, + items: entries.map((entry, entryIndex) => { + const href = getUrlForApp(entry.app, { + path: entry.path, + }); + + const isSelected = + entry.app === currentAppId && + matchPath(currentPath, { + path: entry.path, + }) != null; + + return { + id: `${sectionIndex}.${entryIndex}`, + name: entry.label, + href, + isSelected, + onClick: (event) => { + if ( + event.button !== 0 || + event.defaultPrevented || + event.metaKey || + event.altKey || + event.ctrlKey || + event.shiftKey + ) { + return; + } + + event.preventDefault(); + navigateToApp(entry.app, { + path: entry.path, + }); + }, + }; + }), + })), + [currentAppId, currentPath, getUrlForApp, navigateToApp, sections] + ); + + return ( + + {children} + + ); +} + +// for lazy import +// eslint-disable-next-line import/no-default-export +export default ObservabilityPageTemplate; + +const sideNavTitle = i18n.translate('xpack.observability.pageLayout.sideNavTitle', { + defaultMessage: 'Observability', +}); diff --git a/x-pack/plugins/observability/public/context/plugin_context.tsx b/x-pack/plugins/observability/public/context/plugin_context.tsx index 9b0bacc4c1174d..7c710d1d84c3d8 100644 --- a/x-pack/plugins/observability/public/context/plugin_context.tsx +++ b/x-pack/plugins/observability/public/context/plugin_context.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import { createContext } from 'react'; import { AppMountParameters, CoreStart } from 'kibana/public'; +import { createContext } from 'react'; import { ObservabilityPublicPluginsStart } from '../plugin'; import { ConfigSchema } from '..'; import { ObservabilityRuleTypeRegistry } from '../rules/create_observability_rule_type_registry'; +import type { LazyObservabilityPageTemplateProps } from '../components/shared/page_template/lazy_page_template'; export interface PluginContextValue { appMountParameters: AppMountParameters; @@ -17,6 +18,7 @@ export interface PluginContextValue { core: CoreStart; plugins: ObservabilityPublicPluginsStart; observabilityRuleTypeRegistry: ObservabilityRuleTypeRegistry; + ObservabilityPageTemplate: React.ComponentType; } export const PluginContext = createContext({} as PluginContextValue); diff --git a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts index 8808d6390e3651..7a241401722cf5 100644 --- a/x-pack/plugins/observability/public/hooks/use_time_range.test.ts +++ b/x-pack/plugins/observability/public/hooks/use_time_range.test.ts @@ -24,7 +24,7 @@ describe('useTimeRange', () => { jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ core: {} as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true } } }, + config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, plugins: ({ data: { query: { @@ -40,6 +40,7 @@ describe('useTimeRange', () => { }, } as unknown) as ObservabilityPublicPluginsStart, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: () => null, })); jest.spyOn(kibanaUISettings, 'useKibanaUISettings').mockImplementation(() => ({ from: '2020-10-08T05:00:00.000Z', @@ -66,7 +67,7 @@ describe('useTimeRange', () => { jest.spyOn(pluginContext, 'usePluginContext').mockImplementation(() => ({ core: {} as CoreStart, appMountParameters: {} as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true } } }, + config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, plugins: ({ data: { query: { @@ -82,6 +83,7 @@ describe('useTimeRange', () => { }, } as unknown) as ObservabilityPublicPluginsStart, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: () => null, })); }); it('returns ranges and absolute times from kibana default settings', () => { diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 8dd2f6a57eefe9..a49d3461529c28 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -22,7 +22,7 @@ export type { export { enableInspectEsQueries } from '../common/ui_settings_keys'; export interface ConfigSchema { - unsafe: { alertingExperience: { enabled: boolean } }; + unsafe: { alertingExperience: { enabled: boolean }; cases: { enabled: boolean } }; } export const plugin: PluginInitializer< diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index 7b8055c82f0717..b76e9f82d8dfe7 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -5,15 +5,7 @@ * 2.0. */ -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiPageTemplate, - EuiSpacer, -} from '@elastic/eui'; +import { EuiButton, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ALERT_START, @@ -56,7 +48,7 @@ interface AlertsPageProps { } export function AlertsPage({ routeParams }: AlertsPageProps) { - const { core, observabilityRuleTypeRegistry } = usePluginContext(); + const { core, observabilityRuleTypeRegistry, ObservabilityPageTemplate } = usePluginContext(); const { prepend } = core.http.basePath; const history = useHistory(); const { @@ -131,7 +123,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { } return ( - @@ -139,7 +131,6 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { ), - rightSideItems: [ {i18n.translate('xpack.observability.alerts.manageDetectionRulesButtonLabel', { @@ -206,6 +197,6 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { - +
); } diff --git a/x-pack/plugins/observability/public/pages/cases/index.tsx b/x-pack/plugins/observability/public/pages/cases/index.tsx index dd7f7875b568e4..7f6bce7d486f3a 100644 --- a/x-pack/plugins/observability/public/pages/cases/index.tsx +++ b/x-pack/plugins/observability/public/pages/cases/index.tsx @@ -5,19 +5,21 @@ * 2.0. */ -import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiPageTemplate } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ExperimentalBadge } from '../../components/shared/experimental_badge'; import { RouteParams } from '../../routes'; +import { usePluginContext } from '../../hooks/use_plugin_context'; interface CasesProps { routeParams: RouteParams<'/cases'>; } export function CasesPage(props: CasesProps) { + const { ObservabilityPageTemplate } = usePluginContext(); return ( - @@ -44,6 +46,6 @@ export function CasesPage(props: CasesProps) { - + ); } diff --git a/x-pack/plugins/observability/public/pages/landing/index.tsx b/x-pack/plugins/observability/public/pages/landing/index.tsx index 73693e14d6ac1e..46c99bffbcc698 100644 --- a/x-pack/plugins/observability/public/pages/landing/index.tsx +++ b/x-pack/plugins/observability/public/pages/landing/index.tsx @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import styled, { ThemeContext } from 'styled-components'; import { FleetPanel } from '../../components/app/fleet_panel'; -import { WithHeaderLayout } from '../../components/app/layout/with_header'; +import { ObservabilityHeaderMenu } from '../../components/app/header'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTrackPageview } from '../../hooks/use_track_metric'; import { appsSection } from '../home/section'; @@ -34,15 +34,12 @@ export function LandingPage() { useTrackPageview({ app: 'observability-overview', path: 'landing' }); useTrackPageview({ app: 'observability-overview', path: 'landing', delay: 15000 }); - const { core } = usePluginContext(); + const { core, ObservabilityPageTemplate } = usePluginContext(); const theme = useContext(ThemeContext); return ( - + + {/* title and description */} @@ -128,6 +125,6 @@ export function LandingPage() { - + ); } diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 7aa473d0ebee96..4cb6792d501952 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -6,12 +6,12 @@ */ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useContext } from 'react'; -import { ThemeContext } from 'styled-components'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; import { useTrackPageview } from '../..'; import { Alert } from '../../../../alerting/common'; import { EmptySections } from '../../components/app/empty_sections'; -import { WithHeaderLayout } from '../../components/app/layout/with_header'; +import { ObservabilityHeaderMenu } from '../../components/app/header'; import { NewsFeed } from '../../components/app/news_feed'; import { Resources } from '../../components/app/resources'; import { AlertsSection } from '../../components/app/section/alerts'; @@ -39,8 +39,7 @@ function calculateBucketSize({ start, end }: { start?: number; end?: number }) { export function OverviewPage({ routeParams }: Props) { useTrackPageview({ app: 'observability-overview', path: 'overview' }); useTrackPageview({ app: 'observability-overview', path: 'overview', delay: 15000 }); - const { core } = usePluginContext(); - const theme = useContext(ThemeContext); + const { core, ObservabilityPageTemplate } = usePluginContext(); const { relativeStart, relativeEnd, absoluteStart, absoluteEnd } = useTimeRange(); @@ -65,18 +64,20 @@ export function OverviewPage({ routeParams }: Props) { }); return ( - - } + , + ], + }} > + {/* Data sections */} @@ -107,6 +108,10 @@ export function OverviewPage({ routeParams }: Props) { - + ); } + +const overviewPageTitle = i18n.translate('xpack.observability.overview.pageTitle', { + defaultMessage: 'Overview', +}); diff --git a/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx index 36c9589e7e169f..e2d691c647acc7 100644 --- a/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx +++ b/x-pack/plugins/observability/public/pages/overview/loading_observability.tsx @@ -5,49 +5,33 @@ * 2.0. */ -import React, { useContext } from 'react'; -import styled, { ThemeContext } from 'styled-components'; -import { EuiFlexItem } from '@elastic/eui'; -import { EuiPanel } from '@elastic/eui'; -import { EuiFlexGroup } from '@elastic/eui'; -import { EuiLoadingSpinner } from '@elastic/eui'; -import { EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { WithHeaderLayout } from '../../components/app/layout/with_header'; - -const CentralizedFlexGroup = styled(EuiFlexGroup)` - justify-content: center; - align-items: center; - // place the element in the center of the page - min-height: calc(100vh - ${(props) => props.theme.eui.euiHeaderChildSize}); -`; +import React from 'react'; +import { ObservabilityHeaderMenu } from '../../components/app/header'; +import { usePluginContext } from '../../hooks/use_plugin_context'; export function LoadingObservability() { - const theme = useContext(ThemeContext); + const { ObservabilityPageTemplate } = usePluginContext(); return ( - - + + + - - - - - - - - {i18n.translate('xpack.observability.overview.loadingObservability', { - defaultMessage: 'Loading Observability', - })} - - - - + + + + {observabilityLoadingMessage} - - + + ); } + +const observabilityLoadingMessage = i18n.translate( + 'xpack.observability.overview.loadingObservability', + { + defaultMessage: 'Loading Observability', + } +); diff --git a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx index ebd1c738591690..2482ae7a8e7abc 100644 --- a/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx +++ b/x-pack/plugins/observability/public/pages/overview/overview.stories.tsx @@ -24,6 +24,7 @@ import { emptyResponse as emptyMetricsResponse, fetchMetricsData } from './mock/ import { newsFeedFetchData } from './mock/news_feed.mock'; import { emptyResponse as emptyUptimeResponse, fetchUptimeData } from './mock/uptime.mock'; import { createObservabilityRuleTypeRegistryMock } from '../../rules/observability_rule_type_registry_mock'; +import { KibanaPageTemplate } from '../../../../../../src/plugins/kibana_react/public'; function unregisterAll() { unregisterDataHandler({ appName: 'apm' }); @@ -45,7 +46,7 @@ const withCore = makeDecorator({ appMountParameters: ({ setHeaderActionMenu: () => {}, } as unknown) as AppMountParameters, - config: { unsafe: { alertingExperience: { enabled: true } } }, + config: { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }, core: options as CoreStart, plugins: ({ data: { @@ -55,6 +56,7 @@ const withCore = makeDecorator({ }, } as unknown) as ObservabilityPublicPluginsStart, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + ObservabilityPageTemplate: KibanaPageTemplate, }} > diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 6856bc97b4a360..c1b18e37faee8a 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, @@ -31,9 +31,11 @@ import type { import type { LensPublicStart } from '../../lens/public'; import { registerDataHandler } from './data_handler'; import { createCallObservabilityApi } from './services/call_observability_api'; +import { createNavigationRegistry } from './services/navigation_registry'; import { toggleOverviewLinkInNav } from './toggle_overview_link_in_nav'; import { ConfigSchema } from '.'; import { createObservabilityRuleTypeRegistry } from './rules/create_observability_rule_type_registry'; +import { createLazyObservabilityPageTemplate } from './components/shared'; export type ObservabilityPublicSetup = ReturnType; @@ -50,7 +52,7 @@ export interface ObservabilityPublicPluginsStart { lens: LensPublicStart; } -export type ObservabilityPublicStart = void; +export type ObservabilityPublicStart = ReturnType; export class Plugin implements @@ -61,13 +63,14 @@ export class Plugin ObservabilityPublicPluginsStart > { private readonly appUpdater$ = new BehaviorSubject(() => ({})); + private readonly navigationRegistry = createNavigationRegistry(); constructor(private readonly initializerContext: PluginInitializerContext) { this.initializerContext = initializerContext; } public setup( - coreSetup: CoreSetup, + coreSetup: CoreSetup, pluginsSetup: ObservabilityPublicPluginsSetup ) { const category = DEFAULT_APP_CATEGORIES.observability; @@ -84,7 +87,7 @@ export class Plugin // Load application bundle const { renderApp } = await import('./application'); // Get start services - const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + const [coreStart, pluginsStart, { navigation }] = await coreSetup.getStartServices(); return renderApp({ config, @@ -92,6 +95,7 @@ export class Plugin plugins: pluginsStart, appMountParameters: params, observabilityRuleTypeRegistry, + ObservabilityPageTemplate: navigation.PageTemplate, }); }; @@ -119,7 +123,9 @@ export class Plugin mount, updater$, }); + } + if (config.unsafe.cases.enabled) { coreSetup.application.register({ id: 'observability-cases', title: 'Cases', @@ -162,13 +168,39 @@ export class Plugin }); } + this.navigationRegistry.registerSections( + of([ + { + label: '', + sortKey: 100, + entries: [{ label: 'Overview', app: 'observability-overview', path: '/overview' }], + }, + ]) + ); + return { dashboard: { register: registerDataHandler }, observabilityRuleTypeRegistry, isAlertingExperienceEnabled: () => config.unsafe.alertingExperience.enabled, + navigation: { + registerSections: this.navigationRegistry.registerSections, + }, }; } public start({ application }: CoreStart) { toggleOverviewLinkInNav(this.appUpdater$, application); + + const PageTemplate = createLazyObservabilityPageTemplate({ + currentAppId$: application.currentAppId$, + getUrlForApp: application.getUrlForApp, + navigateToApp: application.navigateToApp, + navigationSections$: this.navigationRegistry.sections$, + }); + + return { + navigation: { + PageTemplate, + }, + }; } } diff --git a/x-pack/plugins/observability/public/services/navigation_registry.test.ts b/x-pack/plugins/observability/public/services/navigation_registry.test.ts new file mode 100644 index 00000000000000..8e46ed8aacabe7 --- /dev/null +++ b/x-pack/plugins/observability/public/services/navigation_registry.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { firstValueFrom } from '@kbn/std'; +import { of } from 'rxjs'; +import { createNavigationRegistry } from './navigation_registry'; + +describe('Navigation registry', () => { + it('Allows the registration of, and access to, navigation sections', async () => { + const navigationRegistry = createNavigationRegistry(); + + navigationRegistry.registerSections( + of([ + { + label: 'Test A', + sortKey: 100, + entries: [ + { label: 'Url A', app: 'TestA', path: '/url-a' }, + { label: 'Url B', app: 'TestA', path: '/url-b' }, + ], + }, + { + label: 'Test B', + sortKey: 200, + entries: [ + { label: 'Url A', app: 'TestB', path: '/url-a' }, + { label: 'Url B', app: 'TestB', path: '/url-b' }, + ], + }, + ]) + ); + + const sections = await firstValueFrom(navigationRegistry.sections$); + + expect(sections).toEqual([ + { + label: 'Test A', + sortKey: 100, + entries: [ + { + label: 'Url A', + app: 'TestA', + path: '/url-a', + }, + { + label: 'Url B', + app: 'TestA', + path: '/url-b', + }, + ], + }, + { + label: 'Test B', + sortKey: 200, + entries: [ + { + label: 'Url A', + app: 'TestB', + path: '/url-a', + }, + { + label: 'Url B', + app: 'TestB', + path: '/url-b', + }, + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/observability/public/services/navigation_registry.ts b/x-pack/plugins/observability/public/services/navigation_registry.ts new file mode 100644 index 00000000000000..f42f34fcfe9bb3 --- /dev/null +++ b/x-pack/plugins/observability/public/services/navigation_registry.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 { combineLatest, Observable, ReplaySubject } from 'rxjs'; +import { map, scan, shareReplay, switchMap } from 'rxjs/operators'; + +export interface NavigationSection { + label: string | undefined; + sortKey: number; + entries: NavigationEntry[]; +} + +export interface NavigationEntry { + label: string; + app: string; + path: string; +} + +export interface NavigationRegistry { + registerSections: (sections$: Observable) => void; + sections$: Observable; +} + +export const createNavigationRegistry = (): NavigationRegistry => { + const registeredSections$ = new ReplaySubject>(); + + const registerSections = (sections$: Observable) => { + registeredSections$.next(sections$); + }; + + const sections$: Observable = registeredSections$.pipe( + scan( + (accumulatedSections$, newSections) => accumulatedSections$.add(newSections), + new Set>() + ), + switchMap((registeredSections) => combineLatest([...registeredSections])), + map((registeredSections) => + registeredSections.flat().sort((first, second) => first.sortKey - second.sortKey) + ), + shareReplay(1) + ); + + return { + registerSections, + sections$, + }; +}; diff --git a/x-pack/plugins/observability/public/utils/test_helper.tsx b/x-pack/plugins/observability/public/utils/test_helper.tsx index ef7c62a143f250..feacb011e0701c 100644 --- a/x-pack/plugins/observability/public/utils/test_helper.tsx +++ b/x-pack/plugins/observability/public/utils/test_helper.tsx @@ -10,7 +10,10 @@ import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import { IntlProvider } from 'react-intl'; import { of } from 'rxjs'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; +import { + KibanaContextProvider, + KibanaPageTemplate, +} from '../../../../../src/plugins/kibana_react/public'; import translations from '../../../translations/translations/ja-JP.json'; import { PluginContext } from '../context/plugin_context'; import { ObservabilityPublicPluginsStart } from '../plugin'; @@ -31,7 +34,7 @@ export const core = ({ }, } as unknown) as CoreStart; -const config = { unsafe: { alertingExperience: { enabled: true } } }; +const config = { unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } } }; const plugins = ({ data: { query: { timefilter: { timefilter: { setTime: jest.fn() } } } }, @@ -44,7 +47,14 @@ export const render = (component: React.ReactNode) => { {component} diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index ec471df164fe1d..52a60a92f5b957 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -17,7 +17,7 @@ export * from './types'; export const config = { exposeToBrowser: { - unsafe: { alertingExperience: { enabled: true } }, + unsafe: { alertingExperience: { enabled: true }, cases: { enabled: true } }, }, schema: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -27,6 +27,7 @@ export const config = { }), unsafe: schema.object({ alertingExperience: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), + cases: schema.object({ enabled: schema.boolean({ defaultValue: false }) }), }), }), }; diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index 046a9a62d5fa79..9eff1b08cead98 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -57,7 +57,7 @@ export class ObservabilityPlugin implements Plugin { return coreStart.elasticsearch.client.asInternalUser; }, ready: () => Promise.resolve(), - alias: plugins.ruleRegistry.getFullAssetName(), + alias: plugins.ruleRegistry.ruleDataService.getFullAssetName(), }); registerRoutes({ diff --git a/x-pack/plugins/reporting/server/config/index.test.ts b/x-pack/plugins/reporting/server/config/index.test.ts index cba64500575aaf..327b03d679caed 100644 --- a/x-pack/plugins/reporting/server/config/index.test.ts +++ b/x-pack/plugins/reporting/server/config/index.test.ts @@ -15,7 +15,7 @@ const applyReportingDeprecations = (settings: Record = {}) => { const deprecationMessages: string[] = []; const _config: any = {}; _config[CONFIG_PATH] = settings; - const migrated = applyDeprecations( + const { config: migrated } = applyDeprecations( _config, deprecations.map((deprecation) => ({ deprecation, @@ -45,7 +45,7 @@ describe('deprecations', () => { const { messages } = applyReportingDeprecations({ roles: { enabled: true } }); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.reporting.roles\\" is deprecated. Granting reporting privilege through a \\"reporting_user\\" role will not be supported starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privilege to users through feature controls in Management > Security > Roles", + "\\"xpack.reporting.roles\\" is deprecated. Granting reporting privilege through a \\"reporting_user\\" role will not be supported starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privileges to users using Kibana application privileges **Management > Security > Roles**.", ] `); }); diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index 10d7ba5059f837..8927bd8ee94d50 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -35,8 +35,8 @@ export const config: PluginConfigDescriptor = { addDeprecation({ message: `"${fromPath}.roles" is deprecated. Granting reporting privilege through a "reporting_user" role will not be supported ` + - `starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privilege to users ` + - `through feature controls in Management > Security > Roles`, + `starting in 8.0. Please set 'xpack.reporting.roles.enabled' to 'false' and grant reporting privileges to users ` + + `using Kibana application privileges **Management > Security > Roles**.`, }); } }, diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 2d55a4aa7fa6da..b7f3ebe9dcfa8b 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -25,14 +25,14 @@ import { SecurityPluginSetup } from '../../security/server'; import { DEFAULT_SPACE_ID } from '../../spaces/common/constants'; import { SpacesPluginSetup } from '../../spaces/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { ReportingConfig } from './'; +import { ReportingConfig, ReportingSetup } from './'; import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; import { ReportingConfigType } from './config'; import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/screenshots'; import { ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; -import { ReportingPluginRouter, ReportingStart } from './types'; +import { ReportingPluginRouter } from './types'; export interface ReportingInternalSetup { basePath: Pick; @@ -69,7 +69,7 @@ export class ReportingCore { private config?: ReportingConfig; // final config, includes dynamic values based on OS type private executing: Set; - public getStartContract: () => ReportingStart; + public getContract: () => ReportingSetup; constructor(private logger: LevelLogger, context: PluginInitializerContext) { const syncConfig = context.config.get(); @@ -77,11 +77,9 @@ export class ReportingCore { this.executeTask = new ExecuteReportTask(this, syncConfig, this.logger); this.monitorTask = new MonitorReportsTask(this, syncConfig, this.logger); - this.getStartContract = (): ReportingStart => { - return { - usesUiCapabilities: () => syncConfig.roles.enabled === false, - }; - }; + this.getContract = () => ({ + usesUiCapabilities: () => syncConfig.roles.enabled === false, + }); this.executing = new Set(); } diff --git a/x-pack/plugins/reporting/server/deprecations.test.ts b/x-pack/plugins/reporting/server/deprecations.test.ts new file mode 100644 index 00000000000000..cce4721b941a0a --- /dev/null +++ b/x-pack/plugins/reporting/server/deprecations.test.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ReportingCore } from '.'; +import { registerDeprecations } from './deprecations'; +import { createMockConfigSchema, createMockReportingCore } from './test_helpers'; +import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { GetDeprecationsContext, IScopedClusterClient } from 'kibana/server'; + +let reportingCore: ReportingCore; +let context: GetDeprecationsContext; +let esClient: jest.Mocked; + +beforeEach(async () => { + const mockReportingConfig = createMockConfigSchema({ roles: { enabled: false } }); + reportingCore = await createMockReportingCore(mockReportingConfig); + esClient = elasticsearchServiceMock.createScopedClusterClient(); + esClient.asCurrentUser.security.getUser = jest.fn().mockResolvedValue({ + body: { xyz: { username: 'normal_user', roles: ['data_analyst'] } }, + }); + context = ({ esClient } as unknown) as GetDeprecationsContext; +}); + +test('logs no deprecations when setup has no issues', async () => { + const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); + expect(await getDeprecations(context)).toMatchInlineSnapshot(`Array []`); +}); + +test('logs a plain message when only a reporting_user role issue is found', async () => { + esClient.asCurrentUser.security.getUser = jest.fn().mockResolvedValue({ + body: { reportron: { username: 'reportron', roles: ['kibana_admin', 'reporting_user'] } }, + }); + + const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); + expect(await getDeprecations(context)).toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.", + "Assign the custom role(s) as desired, and remove the \\"reporting_user\\" role from the user(s).", + ], + }, + "documentationUrl": "https://www.elastic.co/guide/en/kibana/current/secure-reporting.html", + "level": "critical", + "message": "The deprecated \\"reporting_user\\" role has been found for 1 user(s): \\"reportron\\"", + }, + ] + `); +}); + +test('logs multiple entries when multiple reporting_user role issues are found', async () => { + esClient.asCurrentUser.security.getUser = jest.fn().mockResolvedValue({ + body: { + reportron: { username: 'reportron', roles: ['kibana_admin', 'reporting_user'] }, + supercooluser: { username: 'supercooluser', roles: ['kibana_admin', 'reporting_user'] }, + }, + }); + + const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); + expect(await getDeprecations(context)).toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.", + "Assign the custom role(s) as desired, and remove the \\"reporting_user\\" role from the user(s).", + ], + }, + "documentationUrl": "https://www.elastic.co/guide/en/kibana/current/secure-reporting.html", + "level": "critical", + "message": "The deprecated \\"reporting_user\\" role has been found for 2 user(s): \\"reportron\\", \\"supercooluser\\"", + }, + ] + `); +}); + +test('logs an expanded message when a config issue and a reporting_user role issue is found', async () => { + esClient.asCurrentUser.security.getUser = jest.fn().mockResolvedValue({ + body: { reportron: { username: 'reportron', roles: ['kibana_admin', 'reporting_user'] } }, + }); + + const mockReportingConfig = createMockConfigSchema({ roles: { enabled: true } }); + reportingCore = await createMockReportingCore(mockReportingConfig); + + const { getDeprecations } = await registerDeprecations(reportingCore, coreMock.createSetup()); + expect(await getDeprecations(context)).toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Set \\"xpack.reporting.roles.enabled: false\\" in kibana.yml", + "Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.", + "Assign the custom role(s) as desired, and remove the \\"reporting_user\\" role from the user(s).", + ], + }, + "documentationUrl": "https://www.elastic.co/guide/en/kibana/current/secure-reporting.html", + "level": "critical", + "message": "The deprecated \\"reporting_user\\" role has been found for 1 user(s): \\"reportron\\"", + }, + ] + `); +}); diff --git a/x-pack/plugins/reporting/server/deprecations.ts b/x-pack/plugins/reporting/server/deprecations.ts new file mode 100644 index 00000000000000..61074fff012a24 --- /dev/null +++ b/x-pack/plugins/reporting/server/deprecations.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 type { CoreSetup, DeprecationsDetails, RegisterDeprecationsConfig } from 'src/core/server'; +import { ReportingCore } from '.'; + +const deprecatedRole = 'reporting_user'; +const upgradableConfig = 'xpack.reporting.roles.enabled: false'; + +export async function registerDeprecations( + reporting: ReportingCore, + { deprecations: deprecationsService }: CoreSetup +) { + const deprecationsConfig: RegisterDeprecationsConfig = { + getDeprecations: async ({ esClient }) => { + const usingDeprecatedConfig = !reporting.getContract().usesUiCapabilities(); + const deprecations: DeprecationsDetails[] = []; + const { body: users } = await esClient.asCurrentUser.security.getUser(); + + const reportingUsers = Object.entries(users) + .filter(([username, user]) => user.roles.includes(deprecatedRole)) + .map(([, user]) => user.username); + const numReportingUsers = reportingUsers.length; + + if (numReportingUsers > 0) { + const usernames = reportingUsers.join('", "'); + deprecations.push({ + message: `The deprecated "${deprecatedRole}" role has been found for ${numReportingUsers} user(s): "${usernames}"`, + documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/secure-reporting.html', + level: 'critical', + correctiveActions: { + manualSteps: [ + ...(usingDeprecatedConfig ? [`Set "${upgradableConfig}" in kibana.yml`] : []), + `Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.`, + `Assign the custom role(s) as desired, and remove the "${deprecatedRole}" role from the user(s).`, + ], + }, + }); + } + + return deprecations; + }, + }; + + deprecationsService.registerDeprecations(deprecationsConfig); + + return deprecationsConfig; +} diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index efe1d9450bef31..dc0ddf27a53b36 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -10,6 +10,7 @@ import { PLUGIN_ID } from '../common/constants'; import { ReportingCore } from './'; import { initializeBrowserDriverFactory } from './browsers'; import { buildConfig, registerUiSettings, ReportingConfigType } from './config'; +import { registerDeprecations } from './deprecations'; import { LevelLogger, ReportingStore } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; @@ -38,15 +39,13 @@ export class ReportingPlugin // @ts-expect-error null is not assignable to object. use a boolean property to ensure reporting API is enabled. core.http.registerRouteHandlerContext(PLUGIN_ID, () => { if (reportingCore.pluginIsStarted()) { - return reportingCore.getStartContract(); + return reportingCore.getContract(); } else { this.logger.error(`Reporting features are not yet ready`); return null; } }); - registerUiSettings(core); - const { http } = core; const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins; @@ -65,6 +64,8 @@ export class ReportingPlugin logger: this.logger, }); + registerUiSettings(core); + registerDeprecations(reportingCore, core); registerReportingUsageCollector(reportingCore, plugins); registerRoutes(reportingCore, this.logger); @@ -81,7 +82,7 @@ export class ReportingPlugin }); this.reportingCore = reportingCore; - return reportingCore.getStartContract(); + return reportingCore.getContract(); } public start(core: CoreStart, plugins: ReportingStartDeps) { @@ -116,6 +117,6 @@ export class ReportingPlugin this.logger.error(e); }); - return reportingCore.getStartContract(); + return reportingCore.getContract(); } } diff --git a/x-pack/plugins/rule_registry/kibana.json b/x-pack/plugins/rule_registry/kibana.json index 7e3f8bf6afb721..8c1e8d0f5e40ed 100644 --- a/x-pack/plugins/rule_registry/kibana.json +++ b/x-pack/plugins/rule_registry/kibana.json @@ -8,6 +8,8 @@ ], "requiredPlugins": [ "alerting", + "data", + "spaces", "triggersActionsUi" ], "server": true diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts new file mode 100644 index 00000000000000..498b6d16a6fdac --- /dev/null +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const config = { + schema: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + write: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + index: schema.string({ defaultValue: '.alerts' }), + }), +}; + +export type RuleRegistryPluginConfig = TypeOf; diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index.ts new file mode 100644 index 00000000000000..1941208ed07cdf --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/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 * from './index_bootstrapper'; +export * from './index_management_gateway'; +export * from './index_reader'; +export * from './index_writer'; +export * from './resources/ilm_policy'; +export * from './resources/index_mappings'; +export * from './resources/index_names'; diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_bootstrapper.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_bootstrapper.ts new file mode 100644 index 00000000000000..b0c3927cd7dfe6 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_bootstrapper.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { Logger } from 'src/core/server'; + +import { IndexNames } from './resources/index_names'; +import { IndexMappings } from './resources/index_mappings'; +import { createIndexTemplate } from './resources/index_template'; +import { IlmPolicy, defaultIlmPolicy } from './resources/ilm_policy'; +import { IIndexManagementGateway } from './index_management_gateway'; + +interface ConstructorParams { + gateway: IIndexManagementGateway; + logger: Logger; +} + +export interface IndexSpecification { + indexNames: IndexNames; + indexMappings: IndexMappings; + ilmPolicy?: IlmPolicy; +} + +export type IIndexBootstrapper = PublicMethodsOf; + +// TODO: Converge with the logic of .siem-signals index bootstrapping +// x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts + +// TODO: Handle race conditions and potential errors between multiple instances of Kibana +// trying to bootstrap the same index. Possible options: +// - robust idempotent logic with error handling +// - leveraging task_manager to make sure bootstrapping is run only once at a time +// - using some sort of distributed lock +// Maybe we can check how Saved Objects service bootstraps .kibana index + +export class IndexBootstrapper { + private readonly gateway: IIndexManagementGateway; + private readonly logger: Logger; + + constructor(params: ConstructorParams) { + this.gateway = params.gateway; + this.logger = params.logger.get('IndexBootstrapper'); + } + + public async run(indexSpec: IndexSpecification): Promise { + this.logger.debug('bootstrapping elasticsearch resources starting'); + + try { + const { indexNames, indexMappings, ilmPolicy } = indexSpec; + await this.createIlmPolicyIfNotExists(indexNames, ilmPolicy); + await this.createIndexTemplateIfNotExists(indexNames, indexMappings); + await this.createInitialIndexIfNotExists(indexNames); + } catch (err) { + this.logger.error(`error bootstrapping elasticsearch resources: ${err.message}`); + return false; + } + + this.logger.debug('bootstrapping elasticsearch resources complete'); + return true; + } + + private async createIlmPolicyIfNotExists(names: IndexNames, policy?: IlmPolicy): Promise { + const { indexIlmPolicyName } = names; + + const exists = await this.gateway.doesIlmPolicyExist(indexIlmPolicyName); + if (!exists) { + const ilmPolicy = policy ?? defaultIlmPolicy; + await this.gateway.createIlmPolicy(indexIlmPolicyName, ilmPolicy); + } + } + + private async createIndexTemplateIfNotExists( + names: IndexNames, + mappings: IndexMappings + ): Promise { + const { indexTemplateName } = names; + + const templateVersion = 1; // TODO: get from EventSchema definition + const template = createIndexTemplate(names, mappings, templateVersion); + + const exists = await this.gateway.doesIndexTemplateExist(indexTemplateName); + if (!exists) { + await this.gateway.createIndexTemplate(indexTemplateName, template); + } else { + await this.gateway.updateIndexTemplate(indexTemplateName, template); + } + } + + private async createInitialIndexIfNotExists(names: IndexNames): Promise { + const { indexAliasName, indexInitialName } = names; + + const exists = await this.gateway.doesAliasExist(indexAliasName); + if (!exists) { + await this.gateway.createIndex(indexInitialName, { + aliases: { + [indexAliasName]: { + is_write_index: true, + }, + }, + }); + } + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_management_gateway.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_management_gateway.ts new file mode 100644 index 00000000000000..96b5860e143eea --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_management_gateway.ts @@ -0,0 +1,139 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license 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 { PublicMethodsOf } from '@kbn/utility-types'; +import { ElasticsearchClient, Logger } from 'src/core/server'; +import { IlmPolicy } from './resources/ilm_policy'; +import { IndexTemplate } from './resources/index_template'; + +interface ConstructorParams { + elasticsearch: Promise; + logger: Logger; +} + +export type IIndexManagementGateway = PublicMethodsOf; + +export class IndexManagementGateway { + private readonly elasticsearch: Promise; + private readonly logger: Logger; + + constructor(params: ConstructorParams) { + this.elasticsearch = params.elasticsearch; + this.logger = params.logger.get('IndexManagementGateway'); + } + + public async doesIlmPolicyExist(policyName: string): Promise { + this.logger.debug(`Checking if ILM policy exists; name="${policyName}"`); + + try { + const es = await this.elasticsearch; + await es.transport.request({ + method: 'GET', + path: `/_ilm/policy/${policyName}`, + }); + } catch (e) { + if (e.statusCode === 404) return false; + throw new Error(`Error checking existence of ILM policy: ${e.message}`); + } + return true; + } + + public async createIlmPolicy(policyName: string, policy: IlmPolicy): Promise { + this.logger.debug(`Creating ILM policy; name="${policyName}"`); + + try { + const es = await this.elasticsearch; + await es.transport.request({ + method: 'PUT', + path: `/_ilm/policy/${policyName}`, + body: policy, + }); + } catch (e) { + throw new Error(`Error creating ILM policy: ${e.message}`); + } + } + + public async doesIndexTemplateExist(templateName: string): Promise { + this.logger.debug(`Checking if index template exists; name="${templateName}"`); + + try { + const es = await this.elasticsearch; + const { body } = await es.indices.existsTemplate({ name: templateName }); + return body as boolean; + } catch (e) { + throw new Error(`Error checking existence of index template: ${e.message}`); + } + } + + public async createIndexTemplate(templateName: string, template: IndexTemplate): Promise { + this.logger.debug(`Creating index template; name="${templateName}"`); + + try { + const es = await this.elasticsearch; + await es.indices.putTemplate({ create: true, name: templateName, body: template }); + } catch (e) { + // The error message doesn't have a type attribute we can look to guarantee it's due + // to the template already existing (only long message) so we'll check ourselves to see + // if the template now exists. This scenario would happen if you startup multiple Kibana + // instances at the same time. + const existsNow = await this.doesIndexTemplateExist(templateName); + if (!existsNow) { + const error = new Error(`Error creating index template: ${e.message}`); + Object.assign(error, { wrapped: e }); + throw error; + } + } + } + + public async updateIndexTemplate(templateName: string, template: IndexTemplate): Promise { + this.logger.debug(`Updating index template; name="${templateName}"`); + + try { + const { settings, ...templateWithoutSettings } = template; + + const es = await this.elasticsearch; + await es.indices.putTemplate({ + create: false, + name: templateName, + body: templateWithoutSettings, + }); + } catch (e) { + throw new Error(`Error updating index template: ${e.message}`); + } + } + + public async doesAliasExist(aliasName: string): Promise { + this.logger.debug(`Checking if index alias exists; name="${aliasName}"`); + + try { + const es = await this.elasticsearch; + const { body } = await es.indices.existsAlias({ name: aliasName }); + return body as boolean; + } catch (e) { + throw new Error(`Error checking existence of initial index: ${e.message}`); + } + } + + public async createIndex(indexName: string, body: Record = {}): Promise { + this.logger.debug(`Creating index; name="${indexName}"`); + this.logger.debug(JSON.stringify(body, null, 2)); + + try { + const es = await this.elasticsearch; + await es.indices.create({ + index: indexName, + body, + }); + } catch (e) { + if (e.body?.error?.type !== 'resource_already_exists_exception') { + this.logger.error(e); + this.logger.error(JSON.stringify(e.meta, null, 2)); + throw new Error(`Error creating initial index: ${e.message}`); + } + } + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_reader.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_reader.ts new file mode 100644 index 00000000000000..84c0b41f7e1a03 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_reader.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { estypes } from '@elastic/elasticsearch'; +import { Logger, ElasticsearchClient } from 'src/core/server'; + +interface ConstructorParams { + indexName: string; + elasticsearch: Promise; + logger: Logger; +} + +export type IIndexReader = PublicMethodsOf; + +export class IndexReader { + private readonly indexName: string; + private readonly elasticsearch: Promise; + private readonly logger: Logger; + + constructor(params: ConstructorParams) { + this.indexName = params.indexName; + this.elasticsearch = params.elasticsearch; + this.logger = params.logger.get('IndexReader'); + } + + public async search(request: estypes.SearchRequest) { + const requestToSend: estypes.SearchRequest = { + ...request, + index: this.indexName, + }; + + this.logger.debug(`Searching; request: ${JSON.stringify(requestToSend, null)}`); + + const esClient = await this.elasticsearch; + const response = await esClient.search(requestToSend); + + return response; + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.ts new file mode 100644 index 00000000000000..7f83421ec80d85 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/index_writer.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 type { PublicMethodsOf } from '@kbn/utility-types'; +import util from 'util'; +import { Logger, ElasticsearchClient } from 'src/core/server'; +import { BufferedStream } from './utils/buffered_stream'; + +type Document = Record; + +interface BufferItem { + index: string; + doc: Document; +} + +interface ConstructorParams { + indexName: string; + elasticsearch: Promise; + isWriteEnabled: boolean; + logger: Logger; +} + +export type IIndexWriter = PublicMethodsOf; + +export class IndexWriter { + private readonly indexName: string; + private readonly elasticsearch: Promise; + private readonly isWriteEnabled: boolean; + private readonly logger: Logger; + private readonly buffer: BufferedStream; + + constructor(params: ConstructorParams) { + this.indexName = params.indexName; + this.elasticsearch = params.elasticsearch; + this.isWriteEnabled = params.isWriteEnabled; + this.logger = params.logger.get('IndexWriter'); + + this.buffer = new BufferedStream({ + flush: (items) => this.bulkIndex(items), + }); + } + + public indexOne(doc: Document): void { + if (this.isWriteEnabled) { + this.logger.debug('Buffering 1 document'); + this.buffer.enqueue({ index: this.indexName, doc }); + } + } + + public indexMany(docs: Document[]): void { + if (this.isWriteEnabled) { + this.logger.debug(`Buffering ${docs.length} documents`); + docs.forEach((doc) => { + this.buffer.enqueue({ index: this.indexName, doc }); + }); + } + } + + public async close(): Promise { + await this.buffer.closeAndWaitUntilFlushed(); + } + + private async bulkIndex(items: BufferItem[]): Promise { + this.logger.debug(`Indexing ${items.length} documents`); + + const bulkBody: Array> = []; + + for (const item of items) { + if (item.doc === undefined) continue; + + bulkBody.push({ create: { _index: item.index } }); + bulkBody.push(item.doc); + } + + try { + const es = await this.elasticsearch; + const response = await es.bulk({ body: bulkBody }); + + if (response.body.errors) { + const error = new Error('Error writing some bulk events'); + error.stack += '\n' + util.inspect(response.body.items, { depth: null }); + this.logger.error(error); + } + } catch (e) { + this.logger.error( + `error writing bulk events: "${e.message}"; docs: ${JSON.stringify(bulkBody)}` + ); + } + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/ilm_policy.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/ilm_policy.ts new file mode 100644 index 00000000000000..00fc9131523ace --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/ilm_policy.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { estypes } from '@elastic/elasticsearch'; + +export interface IlmPolicy { + policy: estypes.Policy; +} + +export const defaultIlmPolicy: IlmPolicy = { + policy: { + phases: { + hot: { + min_age: '0ms', + actions: { + rollover: { + max_age: '90d', + max_size: '50gb', + }, + }, + }, + delete: { + actions: { + delete: {}, + }, + }, + }, + }, +}; diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_mappings.ts similarity index 61% rename from x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts rename to x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_mappings.ts index 7a5f334aee6d2d..064bde5001f7bf 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tokens/adapter_types.ts +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_mappings.ts @@ -5,6 +5,8 @@ * 2.0. */ -export interface CMTokensAdapter { - createEnrollmentTokens(numTokens?: number): Promise; +export interface IndexMappings { + dynamic: 'strict' | boolean; + properties: Record; + _meta?: Record; } diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_names.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_names.ts new file mode 100644 index 00000000000000..1082c62b95e70d --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_names.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface IndexParams { + /** @example '.alerts' */ + indexPrefix: string; + + /** @example 'security', 'security.alerts', 'observability.events' */ + logName: string; + + /** @example 'default' */ + kibanaSpaceId: string; +} + +export interface IndexNames extends IndexParams { + /** @example '.alerts-security.alerts' */ + indexBaseName: string; + + /** @example '.alerts-security.alerts-*' */ + indexBasePattern: string; + + /** @example '.alerts-security.alerts-default' */ + indexAliasName: string; + + /** @example '.alerts-security.alerts-default-*' */ + indexAliasPattern: string; + + /** @example '.alerts-security.alerts-default-policy' */ + indexIlmPolicyName: string; + + /** @example '.alerts-security.alerts-default-template' */ + indexTemplateName: string; + + /** @example '.alerts-security.alerts-default-000001' */ + indexInitialName: string; +} + +export abstract class IndexNames { + public static create(params: IndexParams): IndexNames { + const { indexPrefix, logName, kibanaSpaceId } = params; + + // TODO: validate params + + const indexBaseName = joinWithDash(indexPrefix, logName); + const indexBasePattern = joinWithDash(indexPrefix, logName, '*'); + const indexAliasName = joinWithDash(indexPrefix, logName, kibanaSpaceId); + const indexAliasPattern = joinWithDash(indexPrefix, logName, kibanaSpaceId, '*'); + const indexIlmPolicyName = joinWithDash(indexPrefix, logName, kibanaSpaceId, 'policy'); + const indexTemplateName = joinWithDash(indexPrefix, logName, kibanaSpaceId, 'template'); + const indexInitialName = joinWithDash(indexPrefix, logName, kibanaSpaceId, '000001'); + + return { + indexPrefix, + logName, + kibanaSpaceId, + indexBaseName, + indexBasePattern, + indexAliasName, + indexAliasPattern, + indexIlmPolicyName, + indexTemplateName, + indexInitialName, + }; + } + + public static createChild(parent: IndexNames, logName: string): IndexNames { + return this.create({ + indexPrefix: parent.indexPrefix, + logName: this.createChildLogName(parent.logName, logName), + kibanaSpaceId: parent.kibanaSpaceId, + }); + } + + public static createChildLogName(parentLogName: string, logName: string): string { + return joinWithDot(parentLogName, logName); + } +} + +const joinWithDash = (...names: string[]): string => names.join('-'); +const joinWithDot = (...names: string[]): string => names.join('.'); diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_template.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_template.ts new file mode 100644 index 00000000000000..caf71dadf6e1e5 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/resources/index_template.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 { estypes } from '@elastic/elasticsearch'; +import { IndexNames } from './index_names'; +import { IndexMappings } from './index_mappings'; + +export type IndexTemplate = estypes.PutIndexTemplateRequest['body']; + +export const createIndexTemplate = ( + names: IndexNames, + mappings: IndexMappings, + version: number +): IndexTemplate => { + const { indexAliasName, indexAliasPattern, indexIlmPolicyName } = names; + + return { + index_patterns: [indexAliasPattern], + settings: { + number_of_shards: 1, // TODO: do we need to set this? + auto_expand_replicas: '0-1', // TODO: do we need to set? + index: { + lifecycle: { + name: indexIlmPolicyName, + rollover_alias: indexAliasName, + }, + }, + mapping: { + total_fields: { + limit: 10000, + }, + }, + sort: { + field: '@timestamp', + order: 'desc', + }, + }, + mappings: { + ...mappings, + _meta: { + ...mappings._meta, + version, + }, + }, + version, + }; +}; diff --git a/x-pack/plugins/rule_registry/server/event_log/elasticsearch/utils/buffered_stream.ts b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/utils/buffered_stream.ts new file mode 100644 index 00000000000000..d968cd5a0ac685 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/elasticsearch/utils/buffered_stream.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 { Subject } from 'rxjs'; +import { bufferTime, filter as rxFilter, switchMap } from 'rxjs/operators'; + +export const DEFAULT_BUFFER_TIME_MS = 1000; +export const DEFAULT_BUFFER_SIZE = 100; + +interface ConstructorParams { + maxBufferTimeMs?: number; + maxBufferSize?: number; + flush: (items: TItem[]) => Promise; +} + +// TODO: handle possible exceptions in flush and maybe add retry logic + +export class BufferedStream { + private readonly buffer$: Subject; + private readonly whenBufferCompleteAndFlushed: Promise; + + constructor(params: ConstructorParams) { + const maxBufferTime = params.maxBufferTimeMs ?? DEFAULT_BUFFER_TIME_MS; + const maxBufferSize = params.maxBufferSize ?? DEFAULT_BUFFER_SIZE; + + this.buffer$ = new Subject(); + + // Buffer items for time/buffer length, ignore empty buffers, + // then flush the buffered items; kick things off with a promise + // on the observable, which we'll wait on in shutdown + this.whenBufferCompleteAndFlushed = this.buffer$ + .pipe( + bufferTime(maxBufferTime, null, maxBufferSize), + rxFilter((docs) => docs.length > 0), + switchMap(async (docs) => await params.flush(docs)) + ) + .toPromise(); + } + + public enqueue(item: TItem): void { + this.buffer$.next(item); + } + + public async closeAndWaitUntilFlushed(): Promise { + this.buffer$.complete(); + await this.whenBufferCompleteAndFlushed; + } +} diff --git a/x-pack/plugins/beats_management/public/components/table/controls/index.ts b/x-pack/plugins/rule_registry/server/event_log/event_schema/index.ts similarity index 81% rename from x-pack/plugins/beats_management/public/components/table/controls/index.ts rename to x-pack/plugins/rule_registry/server/event_log/event_schema/index.ts index 72cbc7fcb23fd0..77c041a4059b56 100644 --- a/x-pack/plugins/beats_management/public/components/table/controls/index.ts +++ b/x-pack/plugins/rule_registry/server/event_log/event_schema/index.ts @@ -5,4 +5,5 @@ * 2.0. */ -export { OptionControl } from './option_control'; +export * from './schema_types'; +export * from './schema'; diff --git a/x-pack/plugins/rule_registry/server/event_log/event_schema/schema.ts b/x-pack/plugins/rule_registry/server/event_log/event_schema/schema.ts new file mode 100644 index 00000000000000..9b5d94918a83f6 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/event_schema/schema.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 { EventSchema, Event } from './schema_types'; +import { FieldMap, runtimeTypeFromFieldMap, mergeFieldMaps } from '../../../common/field_map'; +import { + TechnicalRuleFieldMaps, + technicalRuleFieldMap, +} from '../../../common/assets/field_maps/technical_rule_field_map'; + +const baseSchema = createSchema(technicalRuleFieldMap); + +export abstract class Schema { + public static create(fields: TMap): EventSchema { + return createSchema(fields); + } + + public static combine( + s1: EventSchema, + s2: EventSchema + ): EventSchema { + const combinedFields = mergeFieldMaps(s1.objectFields, s2.objectFields); + return createSchema(combinedFields); + } + + public static getBase(): EventSchema { + return baseSchema; + } + + public static extendBase( + fields: TMap + ): EventSchema { + const extensionSchema = createSchema(fields); + return this.combine(baseSchema, extensionSchema); + } +} + +function createSchema(fields: TMap): EventSchema { + const objectType: Event = ({} as unknown) as Event; + const runtimeType = runtimeTypeFromFieldMap(fields); + + return { + objectFields: fields, + objectType, + runtimeType, + }; +} diff --git a/x-pack/plugins/rule_registry/server/event_log/event_schema/schema_types.ts b/x-pack/plugins/rule_registry/server/event_log/event_schema/schema_types.ts new file mode 100644 index 00000000000000..e5c665652fe974 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/event_schema/schema_types.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FieldMap, FieldMapType, TypeOfFieldMap } from '../../../common/field_map'; + +export interface EventSchema { + objectFields: TMap; + objectType: Event; + runtimeType: EventRuntimeType; +} + +export type Event = TypeOfFieldMap; + +export type EventRuntimeType = FieldMapType; + +export { FieldMap }; diff --git a/x-pack/plugins/beats_management/public/components/tag/index.ts b/x-pack/plugins/rule_registry/server/event_log/index.ts similarity index 74% rename from x-pack/plugins/beats_management/public/components/tag/index.ts rename to x-pack/plugins/rule_registry/server/event_log/index.ts index 9bf533537a3b51..cf7467588c22fe 100644 --- a/x-pack/plugins/beats_management/public/components/tag/index.ts +++ b/x-pack/plugins/rule_registry/server/event_log/index.ts @@ -5,5 +5,6 @@ * 2.0. */ -export { TagBadge } from './tag_badge'; -export { TagEdit } from './tag_edit'; +export * from './elasticsearch'; +export * from './event_schema'; +export * from './log'; diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_log.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_log.ts new file mode 100644 index 00000000000000..2b1ecde48d2db0 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_log.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 { estypes } from '@elastic/elasticsearch'; +import { DeepPartial } from '../utils/utility_types'; +import { IndexNames } from '../elasticsearch'; +import { IEventLog, IEventLogger, IEventLoggerTemplate, IEventQueryBuilder } from './public_api'; +import { EventLogParams } from './internal_api'; +import { EventLoggerTemplate } from './event_logger_template'; +import { EventQueryBuilder } from './event_query_builder'; + +export class EventLog implements IEventLog { + private readonly params: EventLogParams; + private readonly initialTemplate: IEventLoggerTemplate; + + constructor(params: EventLogParams) { + this.params = params; + this.initialTemplate = new EventLoggerTemplate({ + ...params, + eventLoggerName: '', + eventFields: {}, + }); + } + + public getNames(): IndexNames { + return this.params.indexNames; + } + + public getLoggerTemplate(fields: DeepPartial): IEventLoggerTemplate { + return this.initialTemplate.getLoggerTemplate(fields); + } + + public getLogger(loggerName: string, fields?: DeepPartial): IEventLogger { + return this.initialTemplate.getLogger(loggerName, fields); + } + + public getQueryBuilder(): IEventQueryBuilder { + return new EventQueryBuilder(this.params); + } + + public async search( + request: estypes.SearchRequest + ): Promise> { + const response = await this.params.indexReader.search(request); + return response.body; + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_log_bootstrapper.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_log_bootstrapper.ts new file mode 100644 index 00000000000000..0498a7cd97b2f5 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_log_bootstrapper.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 { Logger } from 'kibana/server'; +import { IIndexBootstrapper, IndexSpecification } from '../elasticsearch'; + +interface ConstructorParams { + indexSpec: IndexSpecification; + indexBootstrapper: IIndexBootstrapper; + isWriteEnabled: boolean; + logger: Logger; +} + +export class EventLogBootstrapper { + private readonly indexSpec: IndexSpecification; + private readonly indexBootstrapper: IIndexBootstrapper; + private readonly logger: Logger; + private readonly isWriteEnabled: boolean; + private isIndexBootstrapped: boolean; + + constructor(params: ConstructorParams) { + this.indexSpec = params.indexSpec; + this.indexBootstrapper = params.indexBootstrapper; + this.logger = params.logger.get('EventLogBootstrapper'); + this.isWriteEnabled = params.isWriteEnabled; + this.isIndexBootstrapped = false; + } + + public async run(): Promise { + if (this.isIndexBootstrapped || !this.isWriteEnabled) { + return; + } + + const { logName, indexAliasName } = this.indexSpec.indexNames; + const logInfo = `log="${logName}" index="${indexAliasName}"`; + + this.logger.debug(`Bootstrapping started, ${logInfo}`); + this.isIndexBootstrapped = await this.indexBootstrapper.run(this.indexSpec); + this.logger.debug( + `Bootstrapping ${this.isIndexBootstrapped ? 'succeeded' : 'failed'}, ${logInfo}` + ); + + if (!this.isIndexBootstrapped) { + throw new Error(`Event log bootstrapping failed, ${logInfo}`); + } + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_log_definition.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_log_definition.ts new file mode 100644 index 00000000000000..124664d5578b00 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_log_definition.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IlmPolicy, defaultIlmPolicy, IndexNames } from '../elasticsearch'; +import { EventSchema, FieldMap, Schema } from '../event_schema'; +import { EventLogOptions, IEventLogDefinition } from './public_api'; + +export class EventLogDefinition implements IEventLogDefinition { + public readonly eventLogName: string; + public readonly eventSchema: EventSchema; + public readonly ilmPolicy: IlmPolicy; + + constructor(options: EventLogOptions) { + // TODO: validate options; options.name should not contain "-" and "." + this.eventLogName = options.name; + this.eventSchema = options.schema; + this.ilmPolicy = options.ilmPolicy ?? defaultIlmPolicy; + } + + public defineChild( + options: EventLogOptions + ): IEventLogDefinition { + const childName = IndexNames.createChildLogName(this.eventLogName, options.name); + const childSchema = Schema.combine(this.eventSchema, options.schema); + const childPolicy = options.ilmPolicy ?? this.ilmPolicy; + + return new EventLogDefinition({ + name: childName, + schema: childSchema, + ilmPolicy: childPolicy, + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_log_provider.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_log_provider.ts new file mode 100644 index 00000000000000..d1ecd6a977a080 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_log_provider.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IIndexWriter } from '../elasticsearch'; +import { IEventLog } from './public_api'; +import { IEventLogProvider } from './internal_api'; +import { EventLogBootstrapper } from './event_log_bootstrapper'; + +interface ConstructorParams { + log: IEventLog; + logBootstrapper: EventLogBootstrapper; + indexWriter: IIndexWriter; +} + +export class EventLogProvider implements IEventLogProvider { + constructor(private readonly params: ConstructorParams) {} + + public getLog(): IEventLog { + return this.params.log; + } + + public async bootstrapLog(): Promise { + await this.params.logBootstrapper.run(); + } + + public async shutdownLog(): Promise { + await this.params.indexWriter.close(); + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_log_registry.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_log_registry.ts new file mode 100644 index 00000000000000..52f6c6bd918d49 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_log_registry.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 { Event, FieldMap } from '../event_schema'; +import { IEventLogDefinition } from './public_api'; +import { IEventLogRegistry, IEventLogProvider } from './internal_api'; + +const getRegistryKey = (definition: IEventLogDefinition, spaceId: string) => + `${definition.eventLogName}-${spaceId}`; + +interface RegistryEntry { + definition: IEventLogDefinition; + spaceId: string; + provider: IEventLogProvider; +} + +export class EventLogRegistry implements IEventLogRegistry { + private readonly map = new Map(); + + public get( + definition: IEventLogDefinition, + spaceId: string + ): IEventLogProvider> | null { + const key = getRegistryKey(definition, spaceId); + const entry = this.map.get(key); + return entry != null ? (entry.provider as IEventLogProvider>) : null; + } + + public add( + definition: IEventLogDefinition, + spaceId: string, + provider: IEventLogProvider> + ): void { + const key = getRegistryKey(definition, spaceId); + + if (this.map.has(key)) { + throw new Error(`Event log already registered, key="${key}"`); + } + + this.map.set(key, { + definition, + spaceId, + provider, + }); + } + + public async shutdown(): Promise { + const entries = Array.from(this.map.values()); + const promises = entries.map(({ provider }) => provider.shutdownLog()); + await Promise.all(promises); + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_log_resolver.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_log_resolver.ts new file mode 100644 index 00000000000000..8440f554323041 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_log_resolver.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + IndexBootstrapper, + IndexManagementGateway, + IndexNames, + IndexReader, + IndexSpecification, + IndexWriter, +} from '../elasticsearch'; + +import { Event, FieldMap } from '../event_schema'; +import { IEventLogRegistry, IEventLogProvider } from './internal_api'; +import { + EventLogServiceConfig, + EventLogServiceDependencies, + IEventLog, + IEventLogDefinition, + IEventLogResolver, +} from './public_api'; + +import { EventLog } from './event_log'; +import { EventLogBootstrapper } from './event_log_bootstrapper'; +import { EventLogProvider } from './event_log_provider'; +import { mappingFromFieldMap } from './utils/mapping_from_field_map'; + +export class EventLogResolver implements IEventLogResolver { + private readonly indexBootstrapper: IndexBootstrapper; + + constructor( + private readonly config: EventLogServiceConfig, + private readonly deps: EventLogServiceDependencies, + private readonly registry: IEventLogRegistry, + private readonly bootstrapLog: boolean + ) { + this.indexBootstrapper = this.createIndexBootstrapper(); + } + + public async resolve( + definition: IEventLogDefinition, + kibanaSpaceId: string + ): Promise>> { + const provider = this.resolveLogProvider(definition, kibanaSpaceId); + + if (this.bootstrapLog) { + await provider.bootstrapLog(); + } + + return provider.getLog(); + } + + private resolveLogProvider( + definition: IEventLogDefinition, + kibanaSpaceId: string + ): IEventLogProvider> { + const existingProvider = this.registry.get(definition, kibanaSpaceId); + if (existingProvider) { + return existingProvider; + } + + const indexSpec = this.createIndexSpec(definition, kibanaSpaceId); + const indexReader = this.createIndexReader(indexSpec); + const indexWriter = this.createIndexWriter(indexSpec); + const logBootstrapper = this.createEventLogBootstrapper(indexSpec); + const log = this.createEventLog(indexSpec, indexReader, indexWriter); + const logProvider = new EventLogProvider({ + log, + logBootstrapper, + indexWriter, + }); + + this.registry.add(definition, kibanaSpaceId, logProvider); + + return logProvider; + } + + private createIndexSpec( + definition: IEventLogDefinition, + kibanaSpaceId: string + ): IndexSpecification { + const { indexPrefix } = this.config; + const { eventLogName, eventSchema, ilmPolicy } = definition; + + const indexNames = IndexNames.create({ + indexPrefix, + logName: eventLogName, + kibanaSpaceId, + }); + + const indexMappings = mappingFromFieldMap(eventSchema.objectFields); + + return { indexNames, indexMappings, ilmPolicy }; + } + + private createIndexBootstrapper(): IndexBootstrapper { + const { clusterClient, logger } = this.deps; + + return new IndexBootstrapper({ + gateway: new IndexManagementGateway({ + elasticsearch: clusterClient.then((c) => c.asInternalUser), + logger, + }), + logger, + }); + } + + private createIndexReader(indexSpec: IndexSpecification): IndexReader { + const { clusterClient, logger } = this.deps; + const { indexNames } = indexSpec; + + return new IndexReader({ + indexName: indexNames.indexAliasPattern, + elasticsearch: clusterClient.then((c) => c.asInternalUser), // TODO: internal or current? + logger, + }); + } + + private createIndexWriter(indexSpec: IndexSpecification): IndexWriter { + const { clusterClient, logger } = this.deps; + const { isWriteEnabled } = this.config; + const { indexNames } = indexSpec; + + return new IndexWriter({ + indexName: indexNames.indexAliasName, + elasticsearch: clusterClient.then((c) => c.asInternalUser), // TODO: internal or current? + isWriteEnabled, + logger, + }); + } + + private createEventLogBootstrapper(indexSpec: IndexSpecification): EventLogBootstrapper { + const { logger } = this.deps; + const { isWriteEnabled } = this.config; + + return new EventLogBootstrapper({ + indexSpec, + indexBootstrapper: this.indexBootstrapper, + isWriteEnabled, + logger, + }); + } + + private createEventLog( + indexSpec: IndexSpecification, + indexReader: IndexReader, + indexWriter: IndexWriter + ): IEventLog> { + const { logger } = this.deps; + + return new EventLog>({ + indexNames: indexSpec.indexNames, + indexReader, + indexWriter, + logger, + }); + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_log_service.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_log_service.ts new file mode 100644 index 00000000000000..b5b1d23f2e2158 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_log_service.ts @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest } from 'kibana/server'; + +import { Event, FieldMap } from '../event_schema'; +import { + EventLogServiceConfig, + EventLogServiceDependencies, + IEventLog, + IEventLogDefinition, + IEventLogResolver, + IEventLogService, + IScopedEventLogResolver, +} from './public_api'; + +import { EventLogRegistry } from './event_log_registry'; +import { EventLogResolver } from './event_log_resolver'; + +const BOOTSTRAP_BY_DEFAULT = true; + +interface ConstructorParams { + config: EventLogServiceConfig; + dependencies: EventLogServiceDependencies; +} + +export class EventLogService implements IEventLogService { + private readonly registry: EventLogRegistry; + + constructor(private readonly params: ConstructorParams) { + this.registry = new EventLogRegistry(); + } + + public getResolver(bootstrapLog = BOOTSTRAP_BY_DEFAULT): IEventLogResolver { + const { params, registry } = this; + const { config, dependencies } = params; + + return new EventLogResolver(config, dependencies, registry, bootstrapLog); + } + + public getScopedResolver( + request: KibanaRequest, + bootstrapLog = BOOTSTRAP_BY_DEFAULT + ): IScopedEventLogResolver { + const resolver = this.getResolver(bootstrapLog); + + return { + resolve: async ( + definition: IEventLogDefinition + ): Promise>> => { + const spaces = await this.params.dependencies.spacesService; + const spaceId = spaces.getSpaceId(request); + + const log = await resolver.resolve(definition, spaceId); + return log; + }, + }; + } + + public async stop(): Promise { + await this.registry.shutdown(); + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_logger.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_logger.ts new file mode 100644 index 00000000000000..c6f88f49835d70 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_logger.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeepPartial } from '../utils/utility_types'; +import { mergeFields } from '../utils/fields'; +import { EventLoggerParams } from './internal_api'; +import { IEventLogger, IEventLoggerTemplate } from './public_api'; + +export class EventLogger implements IEventLogger { + private readonly params: EventLoggerParams; + private readonly ownTemplate: IEventLoggerTemplate; + + constructor(params: EventLoggerParams, template: IEventLoggerTemplate) { + this.params = params; + this.ownTemplate = template; + } + + public getLoggerTemplate(fields: DeepPartial): IEventLoggerTemplate { + return this.ownTemplate.getLoggerTemplate(fields); + } + + public getLogger(name: string, fields?: DeepPartial): IEventLogger { + return this.ownTemplate.getLogger(name, fields); + } + + public logEvent(fields: DeepPartial): void { + const { eventFields, indexWriter } = this.params; + + const event = mergeFields(eventFields, fields); + indexWriter.indexOne(event); + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_logger_template.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_logger_template.ts new file mode 100644 index 00000000000000..3872a5c744269c --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_logger_template.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 { DeepPartial } from '../utils/utility_types'; +import { mergeFields } from '../utils/fields'; +import { IEventLogger, IEventLoggerTemplate } from './public_api'; +import { EventLoggerParams } from './internal_api'; +import { EventLogger } from './event_logger'; + +export class EventLoggerTemplate implements IEventLoggerTemplate { + private readonly params: EventLoggerParams; + + constructor(params: EventLoggerParams) { + this.params = params; + } + + public getLoggerTemplate(fields: DeepPartial): IEventLoggerTemplate { + const nextParams = this.getNextParams('', fields); + return new EventLoggerTemplate(nextParams); + } + + public getLogger(name: string, fields?: DeepPartial): IEventLogger { + const nextParams = this.getNextParams(name, fields); + const nextTemplate = new EventLoggerTemplate(nextParams); + return new EventLogger(nextParams, nextTemplate); + } + + private getNextParams( + extName: string, + extFields?: DeepPartial + ): EventLoggerParams { + const { indexNames, eventLoggerName, eventFields } = this.params; + + const baseName = eventLoggerName; + const nextName = [baseName, extName].filter(Boolean).join('.'); + + const baseFields = eventFields; + const nextFields = mergeFields(baseFields, extFields, { + // TODO: Define a schema for own fields used/set by event log. Add it to the base schema. + // Then maybe introduce a base type for TEvent. + 'kibana.rac.event_log.log_name': indexNames.logName, + 'kibana.rac.event_log.logger_name': nextName, + } as any); + + return { + ...this.params, + eventLoggerName: nextName, + eventFields: nextFields, + }; + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_query.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_query.ts new file mode 100644 index 00000000000000..0eabe4be64837a --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_query.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 { estypes } from '@elastic/elasticsearch'; +import { IIndexReader } from '../elasticsearch'; +import { truthy } from '../utils/predicates'; +import { IEventQuery } from './public_api'; + +export interface EventQueryParams { + indexReader: IIndexReader; + request: estypes.SearchRequest; +} + +export class EventQuery implements IEventQuery { + constructor(private readonly params: EventQueryParams) {} + + public async execute(): Promise { + const { indexReader, request } = this.params; + + const response = await indexReader.search(request); + return response.body.hits.hits.map((hit) => hit._source).filter(truthy); + } +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/event_query_builder.ts b/x-pack/plugins/rule_registry/server/event_log/log/event_query_builder.ts new file mode 100644 index 00000000000000..48228ce5352b7b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/event_query_builder.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getFlattenedObject } from '@kbn/std'; +import { estypes } from '@elastic/elasticsearch'; +import { esKuery } from '../../../../../../src/plugins/data/server'; + +import { DeepPartial } from '../utils/utility_types'; +import { mergeFields } from '../utils/fields'; +import { EventLogParams } from './internal_api'; +import { IEventQueryBuilder, IEventQuery, SortingParams, PaginationParams } from './public_api'; +import { EventQuery } from './event_query'; + +export class EventQueryBuilder implements IEventQueryBuilder { + private readonly params: EventLogParams; + private loggerName: string; + private fields: DeepPartial | null; + private kql: string; + private sorting: SortingParams; + private pagination: PaginationParams; + + constructor(params: EventLogParams) { + this.params = params; + this.loggerName = ''; + this.fields = null; + this.kql = ''; + this.sorting = [{ '@timestamp': { order: 'desc' } }, { 'event.sequence': { order: 'desc' } }]; + this.pagination = { page: 1, perPage: 20 }; + } + + public filterByLogger(loggerName: string): IEventQueryBuilder { + this.loggerName = loggerName; + return this; + } + + public filterByFields(fields: DeepPartial): IEventQueryBuilder { + this.fields = mergeFields(this.fields ?? {}, fields); + return this; + } + + public filterByKql(kql: string): IEventQueryBuilder { + this.kql = kql; + return this; + } + + public sortBy(params: SortingParams): IEventQueryBuilder { + this.sorting = params; + return this; + } + + public paginate(params: PaginationParams): IEventQueryBuilder { + this.pagination = params; + return this; + } + + public buildQuery(): IEventQuery { + const { indexReader } = this.params; + const { page, perPage } = this.pagination; + + const request: estypes.SearchRequest = { + track_total_hits: true, + body: { + from: (page - 1) * perPage, + size: perPage, + sort: this.sorting, + query: { + bool: { + filter: this.buildFilter(), + }, + }, + }, + }; + + return new EventQuery({ indexReader, request }); + } + + private buildFilter(): estypes.QueryContainer[] { + const result: estypes.QueryContainer[] = []; + + if (this.loggerName) { + result.push({ + term: { 'kibana.rac.event_log.logger_name': this.loggerName }, + }); + } + + if (this.fields) { + const flatFields = getFlattenedObject(this.fields); + Object.entries(flatFields) + .map(([key, value]) => { + const queryName = Array.isArray(value) ? 'terms' : 'term'; + return { [queryName]: { [key]: value } }; + }) + .forEach((query) => { + result.push(query); + }); + } + + if (this.kql) { + const dsl = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(this.kql)); + const queries = Array.isArray(dsl) ? dsl : [dsl]; + result.push(...queries); + } + + return result; + } +} diff --git a/x-pack/plugins/beats_management/public/components/layouts/background.tsx b/x-pack/plugins/rule_registry/server/event_log/log/index.ts similarity index 70% rename from x-pack/plugins/beats_management/public/components/layouts/background.tsx rename to x-pack/plugins/rule_registry/server/event_log/log/index.ts index ff060cb5869faa..e5593390733e4a 100644 --- a/x-pack/plugins/beats_management/public/components/layouts/background.tsx +++ b/x-pack/plugins/rule_registry/server/event_log/log/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import styled from 'styled-components'; - -export const Background = styled.div` - flex-grow: 1; -`; +export * from './event_log_definition'; +export * from './event_log_service'; +export * from './public_api'; diff --git a/x-pack/plugins/rule_registry/server/event_log/log/internal_api.ts b/x-pack/plugins/rule_registry/server/event_log/log/internal_api.ts new file mode 100644 index 00000000000000..8db931b35912d1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/internal_api.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from 'kibana/server'; + +import { IIndexReader, IIndexWriter, IndexNames } from '../elasticsearch'; +import { Event, FieldMap } from '../event_schema'; +import { DeepPartial } from '../utils/utility_types'; +import { IEventLogDefinition, IEventLog } from './public_api'; + +export interface IEventLogRegistry { + get( + definition: IEventLogDefinition, + spaceId: string + ): IEventLogProvider> | null; + + add( + definition: IEventLogDefinition, + spaceId: string, + provider: IEventLogProvider> + ): void; + + shutdown(): Promise; +} + +export interface IEventLogProvider { + getLog(): IEventLog; + bootstrapLog(): Promise; + shutdownLog(): Promise; +} + +export interface EventLogParams { + indexNames: IndexNames; + indexReader: IIndexReader; + indexWriter: IIndexWriter; + logger: Logger; +} + +export interface EventLoggerParams extends EventLogParams { + eventLoggerName: string; + eventFields: DeepPartial; +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/public_api.ts b/x-pack/plugins/rule_registry/server/event_log/log/public_api.ts new file mode 100644 index 00000000000000..7807dd9ca6b36b --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/public_api.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { estypes } from '@elastic/elasticsearch'; +import { IClusterClient, KibanaRequest, Logger } from 'kibana/server'; +import { SpacesServiceStart } from '../../../../spaces/server'; + +import { IlmPolicy, IndexNames, IndexSpecification } from '../elasticsearch'; +import { FieldMap, Event, EventSchema } from '../event_schema'; +import { DeepPartial } from '../utils/utility_types'; + +export { IlmPolicy, IndexSpecification }; + +// ------------------------------------------------------------------------------------------------- +// Definition API (defining log hierarchies as simple objects) + +export interface EventLogOptions { + name: string; + schema: EventSchema; + ilmPolicy?: IlmPolicy; +} + +export interface IEventLogDefinition { + eventLogName: string; + eventSchema: EventSchema; + ilmPolicy: IlmPolicy; + + defineChild( + options: EventLogOptions + ): IEventLogDefinition; +} + +// ------------------------------------------------------------------------------------------------- +// Resolving and bootstrapping API (creating runtime objects representing logs, bootstrapping indices) + +export interface EventLogServiceConfig { + indexPrefix: string; + isWriteEnabled: boolean; +} + +export interface EventLogServiceDependencies { + clusterClient: Promise; + spacesService: Promise; + logger: Logger; +} + +export interface IEventLogService { + getResolver(bootstrapLog?: boolean): IEventLogResolver; + getScopedResolver(request: KibanaRequest, bootstrapLog?: boolean): IScopedEventLogResolver; +} + +export interface IEventLogResolver { + resolve( + definition: IEventLogDefinition, + spaceId: string + ): Promise>>; +} + +export interface IScopedEventLogResolver { + resolve( + definition: IEventLogDefinition + ): Promise>>; +} + +export interface IEventLog extends IEventLoggerTemplate { + getNames(): IndexNames; + + getQueryBuilder(): IEventQueryBuilder; + + search( + request: estypes.SearchRequest + ): Promise>; +} + +// ------------------------------------------------------------------------------------------------- +// Write API (logging events) + +export interface IEventLoggerTemplate { + getLoggerTemplate(fields: DeepPartial): IEventLoggerTemplate; + getLogger(name: string, fields?: DeepPartial): IEventLogger; +} + +export interface IEventLogger extends IEventLoggerTemplate { + logEvent(fields: DeepPartial): void; +} + +// ------------------------------------------------------------------------------------------------- +// Read API (searching, filtering, sorting, pagination, aggregation over events) + +export interface IEventQueryBuilder { + filterByLogger(loggerName: string): IEventQueryBuilder; + filterByFields(fields: DeepPartial): IEventQueryBuilder; + filterByKql(kql: string): IEventQueryBuilder; + sortBy(params: SortingParams): IEventQueryBuilder; + paginate(params: PaginationParams): IEventQueryBuilder; + + buildQuery(): IEventQuery; +} + +export type SortingParams = estypes.Sort; + +export interface PaginationParams { + page: number; + perPage: number; +} + +export interface IEventQuery { + execute(): Promise; +} diff --git a/x-pack/plugins/rule_registry/server/event_log/log/utils/mapping_from_field_map.ts b/x-pack/plugins/rule_registry/server/event_log/log/utils/mapping_from_field_map.ts new file mode 100644 index 00000000000000..fd5dc3ae022881 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/log/utils/mapping_from_field_map.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { set } from '@elastic/safer-lodash-set'; +import { FieldMap } from '../../../../common/field_map'; +import { IndexMappings } from '../../elasticsearch'; + +export function mappingFromFieldMap(fieldMap: FieldMap): IndexMappings { + const mappings = { + dynamic: 'strict' as const, + properties: {}, + }; + + const fields = Object.keys(fieldMap).map((key) => { + const field = fieldMap[key]; + return { + name: key, + ...field, + }; + }); + + fields.forEach((field) => { + const { name, required, array, ...rest } = field; + + set(mappings.properties, field.name.split('.').join('.properties.'), rest); + }); + + return mappings; +} diff --git a/x-pack/plugins/rule_registry/server/event_log/utils/fields.ts b/x-pack/plugins/rule_registry/server/event_log/utils/fields.ts new file mode 100644 index 00000000000000..4f140cfb3434f1 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/utils/fields.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 { merge } from 'lodash'; +import { DeepPartial } from './utility_types'; + +export const mergeFields = ( + base: DeepPartial, + ext1?: DeepPartial, + ext2?: DeepPartial, + ext3?: DeepPartial +): DeepPartial => { + return merge({}, base, ext1 ?? {}, ext2 ?? {}, ext3 ?? {}); +}; diff --git a/x-pack/plugins/rule_registry/server/event_log/utils/predicates.ts b/x-pack/plugins/rule_registry/server/event_log/utils/predicates.ts new file mode 100644 index 00000000000000..40daac1fedcc63 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/utils/predicates.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. + */ + +export function nonNullable(value: T): value is NonNullable { + return value !== null && value !== undefined; +} + +export type Truthy = T extends false | '' | 0 | null | undefined ? never : T; // from lodash + +export function truthy(value: T): value is Truthy { + return Boolean(value); +} diff --git a/x-pack/plugins/rule_registry/server/event_log/utils/ready_signal.test.ts b/x-pack/plugins/rule_registry/server/event_log/utils/ready_signal.test.ts new file mode 100644 index 00000000000000..f01d438ce79a00 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/utils/ready_signal.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReadySignal, ReadySignal } from './ready_signal'; + +describe('ReadySignal', () => { + let readySignal: ReadySignal; + + beforeEach(() => { + readySignal = createReadySignal(); + }); + + test('works as expected', async () => { + readySignal.signal(42); + const ready = await readySignal.wait(); + expect(ready).toBe(42); + }); +}); diff --git a/x-pack/plugins/rule_registry/server/event_log/utils/ready_signal.ts b/x-pack/plugins/rule_registry/server/event_log/utils/ready_signal.ts new file mode 100644 index 00000000000000..0512def2b59779 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/event_log/utils/ready_signal.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. + */ + +export interface ReadySignal { + wait(): Promise; + signal(value: T): void; +} + +export function createReadySignal(): ReadySignal { + let resolver: (value: T) => void; + + const promise = new Promise((resolve) => { + resolver = resolve; + }); + + async function wait(): Promise { + return await promise; + } + + function signal(value: T) { + resolver(value); + } + + return { wait, signal }; +} diff --git a/x-pack/plugins/beats_management/common/constants/table.ts b/x-pack/plugins/rule_registry/server/event_log/utils/utility_types.ts similarity index 55% rename from x-pack/plugins/beats_management/common/constants/table.ts rename to x-pack/plugins/rule_registry/server/event_log/utils/utility_types.ts index 54751c29cd35a6..78e145740da544 100644 --- a/x-pack/plugins/beats_management/common/constants/table.ts +++ b/x-pack/plugins/rule_registry/server/event_log/utils/utility_types.ts @@ -5,9 +5,8 @@ * 2.0. */ -export const TABLE_CONFIG = { - INITIAL_ROW_SIZE: 5, - PAGE_SIZE_OPTIONS: [3, 5, 10, 20], - TRUNCATE_TAG_LENGTH: 33, - TRUNCATE_TAG_LENGTH_SMALL: 20, +export type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }; + +export type DeepPartial = { + [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial; }; diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index b51ba3e10f91a9..9547f165cd7058 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -5,27 +5,15 @@ * 2.0. */ -import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { RuleRegistryPlugin } from './plugin'; +export * from './config'; export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin'; export { RuleDataClient } from './rule_data_client'; export { IRuleDataClient } from './rule_data_client/types'; export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data'; export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory'; -export const config = { - schema: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - write: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - }), - index: schema.string({ defaultValue: '.alerts' }), - }), -}; - -export type RuleRegistryPluginConfig = TypeOf; - export const plugin = (initContext: PluginInitializerContext) => new RuleRegistryPlugin(initContext); diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index 3c645f98f5c719..043b07f9d67c12 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -5,45 +5,99 @@ * 2.0. */ -import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server'; +import { PluginInitializerContext, Plugin, CoreSetup, Logger } from 'src/core/server'; +import { SpacesPluginStart } from '../../spaces/server'; + +import { RuleRegistryPluginConfig } from './config'; import { RuleDataPluginService } from './rule_data_plugin_service'; -import { RuleRegistryPluginConfig } from '.'; +import { EventLogService, IEventLogService } from './event_log'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface RuleRegistryPluginSetupDependencies {} + +interface RuleRegistryPluginStartDependencies { + spaces: SpacesPluginStart; +} + +export interface RuleRegistryPluginSetupContract { + ruleDataService: RuleDataPluginService; + eventLogService: IEventLogService; +} -export type RuleRegistryPluginSetupContract = RuleDataPluginService; export type RuleRegistryPluginStartContract = void; -export class RuleRegistryPlugin implements Plugin { - constructor(private readonly initContext: PluginInitializerContext) { - this.initContext = initContext; +export class RuleRegistryPlugin + implements + Plugin< + RuleRegistryPluginSetupContract, + RuleRegistryPluginStartContract, + RuleRegistryPluginSetupDependencies, + RuleRegistryPluginStartDependencies + > { + private readonly config: RuleRegistryPluginConfig; + private readonly logger: Logger; + private eventLogService: EventLogService | null; + + constructor(initContext: PluginInitializerContext) { + this.config = initContext.config.get(); + this.logger = initContext.logger.get(); + this.eventLogService = null; } - public setup(core: CoreSetup): RuleRegistryPluginSetupContract { - const config = this.initContext.config.get(); + public setup( + core: CoreSetup + ): RuleRegistryPluginSetupContract { + const { config, logger } = this; - const logger = this.initContext.logger.get(); + const startDependencies = core.getStartServices().then(([coreStart, pluginStart]) => { + return { + core: coreStart, + ...pluginStart, + }; + }); - const service = new RuleDataPluginService({ + const ruleDataService = new RuleDataPluginService({ logger, isWriteEnabled: config.write.enabled, index: config.index, getClusterClient: async () => { - const [coreStart] = await core.getStartServices(); - - return coreStart.elasticsearch.client.asInternalUser; + const deps = await startDependencies; + return deps.core.elasticsearch.client.asInternalUser; }, }); - service.init().catch((originalError) => { + ruleDataService.init().catch((originalError) => { const error = new Error('Failed installing assets'); // @ts-ignore error.stack = originalError.stack; logger.error(error); }); - return service; + const eventLogService = new EventLogService({ + config: { + indexPrefix: this.config.index, + isWriteEnabled: this.config.write.enabled, + }, + dependencies: { + clusterClient: startDependencies.then((deps) => deps.core.elasticsearch.client), + spacesService: startDependencies.then((deps) => deps.spaces.spacesService), + logger: logger.get('eventLog'), + }, + }); + + this.eventLogService = eventLogService; + return { ruleDataService, eventLogService }; } public start(): RuleRegistryPluginStartContract {} - public stop() {} + public stop() { + const { eventLogService, logger } = this; + + if (eventLogService) { + eventLogService.stop().catch((e) => { + logger.error(e); + }); + } + } } diff --git a/x-pack/plugins/rule_registry/tsconfig.json b/x-pack/plugins/rule_registry/tsconfig.json index 707e1ccb98dad9..5aefe9769da22b 100644 --- a/x-pack/plugins/rule_registry/tsconfig.json +++ b/x-pack/plugins/rule_registry/tsconfig.json @@ -10,7 +10,9 @@ "include": ["common/**/*", "server/**/*", "public/**/*", "../../../typings/**/*"], "references": [ { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, { "path": "../alerting/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, { "path": "../triggers_actions_ui/tsconfig.json" } ] } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index e06e40b86e01be..861f6900fda589 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -20,8 +20,14 @@ describe(`feature_privilege_builder`, () => { const privilege: FeatureKibanaPrivileges = { alerting: { - all: [], - read: [], + rule: { + all: [], + read: [], + }, + alert: { + all: [], + read: [], + }, }, savedObject: { @@ -46,14 +52,16 @@ describe(`feature_privilege_builder`, () => { }); describe(`within feature`, () => { - test('grants `read` privileges under feature consumer', () => { + test('grants `read` privileges to rules under feature consumer', () => { const actions = new Actions(version); const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); const privilege: FeatureKibanaPrivileges = { alerting: { - all: [], - read: ['alert-type'], + rule: { + all: [], + read: ['alert-type'], + }, }, savedObject: { @@ -80,20 +88,20 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", ] `); }); - test('grants `all` privileges under feature consumer', () => { + test('grants `read` privileges to alerts under feature consumer', () => { const actions = new Actions(version); const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); const privilege: FeatureKibanaPrivileges = { alerting: { - all: ['alert-type'], - read: [], + alert: { + all: [], + read: ['alert-type'], + }, }, savedObject: { @@ -116,35 +124,26 @@ describe(`feature_privilege_builder`, () => { expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` Array [ - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", ] `); }); - test('grants both `all` and `read` privileges under feature consumer', () => { + test('grants `read` privileges to rules and alerts under feature consumer', () => { const actions = new Actions(version); const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); const privilege: FeatureKibanaPrivileges = { alerting: { - all: ['alert-type'], - read: ['readonly-alert-type'], + rule: { + all: [], + read: ['alert-type'], + }, + alert: { + all: [], + read: ['alert-type'], + }, }, savedObject: { @@ -171,28 +170,315 @@ describe(`feature_privilege_builder`, () => { "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert", - "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", - "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleState", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", - "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", ] `); }); + + test('grants `all` privileges to rules under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + rule: { + all: ['alert-type'], + read: [], + }, + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert", + ] + `); + }); + + test('grants `all` privileges to alerts under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + alert: { + all: ['alert-type'], + read: [], + }, + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", + ] + `); + }); + + test('grants `all` privileges to rules and alerts under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + rule: { + all: ['alert-type'], + read: [], + }, + alert: { + all: ['alert-type'], + read: [], + }, + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", + ] + `); + }); + + test('grants both `all` and `read` to rules privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + rule: { + all: ['alert-type'], + read: ['readonly-alert-type'], + }, + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleState", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", + ] + `); + }); + + test('grants both `all` and `read` to alerts privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + alert: { + all: ['alert-type'], + read: ['readonly-alert-type'], + }, + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/alert/update", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", + ] + `); + }); + + test('grants both `all` and `read` to rules and alerts privileges under feature consumer', () => { + const actions = new Actions(version); + const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions); + + const privilege: FeatureKibanaPrivileges = { + alerting: { + rule: { + all: ['alert-type'], + read: ['readonly-alert-type'], + }, + alert: { + all: ['another-alert-type'], + read: ['readonly-alert-type'], + }, + }, + + savedObject: { + all: [], + read: [], + }, + ui: [], + }; + + const feature = new KibanaFeature({ + id: 'my-feature', + name: 'my-feature', + app: [], + category: { id: 'foo', label: 'foo' }, + privileges: { + all: privilege, + read: privilege, + }, + }); + + expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(` + Array [ + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getRuleState", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/getAlertSummary", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/create", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/delete", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/update", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/updateApiKey", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/enable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/disable", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAll", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/muteAlert", + "alerting:1.0.0-zeta1:alert-type/my-feature/rule/unmuteAlert", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getRuleState", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/getAlertSummary", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/rule/find", + "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/find", + "alerting:1.0.0-zeta1:another-alert-type/my-feature/alert/update", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/get", + "alerting:1.0.0-zeta1:readonly-alert-type/my-feature/alert/find", + ] + `); + }); }); }); }); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index 1d0a2b0e129434..f536959a910cd1 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -5,22 +5,22 @@ * 2.0. */ -import { uniq } from 'lodash'; +import { get, uniq } from 'lodash'; import type { FeatureKibanaPrivileges, KibanaFeature } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; -enum AlertingType { +enum AlertingEntity { RULE = 'rule', ALERT = 'alert', } -const readOperations: Record = { +const readOperations: Record = { rule: ['get', 'getRuleState', 'getAlertSummary', 'find'], alert: ['get', 'find'], }; -const writeOperations: Record = { +const writeOperations: Record = { rule: [ 'create', 'delete', @@ -35,7 +35,7 @@ const writeOperations: Record = { ], alert: ['update'], }; -const allOperations: Record = { +const allOperations: Record = { rule: [...readOperations.rule, ...writeOperations.rule], alert: [...readOperations.alert, ...writeOperations.alert], }; @@ -46,21 +46,30 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder feature: KibanaFeature ): string[] { const getAlertingPrivilege = ( - operations: Record, + operations: string[], privilegedTypes: readonly string[], + alertingEntity: string, consumer: string ) => - privilegedTypes.flatMap((privilegedType) => - Object.values(AlertingType).flatMap((alertingType) => - operations[alertingType].map((operation) => - this.actions.alerting.get(privilegedType, consumer, alertingType, operation) - ) + privilegedTypes.flatMap((type) => + operations.map((operation) => + this.actions.alerting.get(type, consumer, alertingEntity, operation) ) ); + const getPrivilegesForEntity = (entity: AlertingEntity) => { + const all = get(privilegeDefinition.alerting, `${entity}.all`) ?? []; + const read = get(privilegeDefinition.alerting, `${entity}.read`) ?? []; + + return uniq([ + ...getAlertingPrivilege(allOperations[entity], all, entity, feature.id), + ...getAlertingPrivilege(readOperations[entity], read, entity, feature.id), + ]); + }; + return uniq([ - ...getAlertingPrivilege(allOperations, privilegeDefinition.alerting?.all ?? [], feature.id), - ...getAlertingPrivilege(readOperations, privilegeDefinition.alerting?.read ?? [], feature.id), + ...getPrivilegesForEntity(AlertingEntity.RULE), + ...getPrivilegesForEntity(AlertingEntity.ALERT), ]); } } diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index 80173dd42a49e4..a233d760359e5b 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -14,7 +14,7 @@ import { securityConfigDeprecationProvider } from './config_deprecations'; const applyConfigDeprecations = (settings: Record = {}) => { const deprecations = securityConfigDeprecationProvider(configDeprecationFactory); const deprecationMessages: string[] = []; - const migrated = applyDeprecations( + const { config: migrated } = applyDeprecations( settings, deprecations.map((deprecation) => ({ deprecation, diff --git a/x-pack/plugins/security_solution/common/endpoint/actions.ts b/x-pack/plugins/security_solution/common/endpoint/actions.ts new file mode 100644 index 00000000000000..287ebddacad9a0 --- /dev/null +++ b/x-pack/plugins/security_solution/common/endpoint/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. + */ + +export const userCanIsolate = (roles: readonly string[] | undefined): boolean => { + // only superusers can write to the fleet index (or look up endpoint data to convert endp ID to agent ID) + if (!roles || roles.length === 0) { + return false; + } + return roles.includes('superuser'); +}; diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index a5718af1d42c53..1c0b09a4648e5f 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -15,6 +15,9 @@ export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency'; export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; +export const HOST_METADATA_LIST_API = '/api/endpoint/metadata'; +export const HOST_METADATA_GET_API = '/api/endpoint/metadata/{id}'; + export const TRUSTED_APPS_GET_API = '/api/endpoint/trusted_apps/{id}'; export const TRUSTED_APPS_LIST_API = '/api/endpoint/trusted_apps'; export const TRUSTED_APPS_CREATE_API = '/api/endpoint/trusted_apps'; 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 4a86d7fd4de774..fa7ee84441a9bc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1302,6 +1302,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { */ public generateEpmPackage(): GetPackagesResponse['response'][0] { return { + id: this.seededUUIDv4(), name: 'endpoint', title: 'Elastic Endpoint', version: '0.5.0', diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 512adffc70eef4..dd0ff540cb4af0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -414,7 +414,7 @@ export type PolicyInfo = Immutable<{ id: string; }>; -export interface HostMetaDataInfo { +export interface HostMetadataInfo { metadata: HostMetadata; query_strategy_version: MetadataQueryStrategyVersions; } diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts index de51df283251d2..f5387a1b1a99c6 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/index.ts @@ -7,3 +7,4 @@ export * from './isolate_success'; export * from './isolate_form'; +export * from './unisolate_form'; diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx index 32ac1177d6e80b..f822b3c287a02a 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/isolate_success.tsx @@ -7,27 +7,44 @@ import React, { memo, ReactNode } from 'react'; import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { GET_SUCCESS_MESSAGE } from './translations'; +import { GET_ISOLATION_SUCCESS_MESSAGE, GET_UNISOLATION_SUCCESS_MESSAGE } from './translations'; export interface EndpointIsolateSuccessProps { hostName: string; + isolateAction?: 'isolateHost' | 'unisolateHost'; completeButtonLabel: string; onComplete: () => void; additionalInfo?: ReactNode; } export const EndpointIsolateSuccess = memo( - ({ hostName, onComplete, completeButtonLabel, additionalInfo }) => { + ({ + hostName, + isolateAction = 'isolateHost', + onComplete, + completeButtonLabel, + additionalInfo, + }) => { return ( <> - - {additionalInfo} - + {isolateAction === 'isolateHost' ? ( + + {additionalInfo} + + ) : ( + + {additionalInfo} + + )} diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts index baeced2a7a69fb..790c951f61ccd9 100644 --- a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/translations.ts @@ -24,8 +24,33 @@ export const COMMENT_PLACEHOLDER = i18n.translate( { defaultMessage: 'You may leave an optional note here.' } ); -export const GET_SUCCESS_MESSAGE = (hostName: string) => - i18n.translate('xpack.securitySolution.endpoint.hostIsolation.successfulMessage', { +export const GET_ISOLATION_SUCCESS_MESSAGE = (hostName: string) => + i18n.translate('xpack.securitySolution.endpoint.hostIsolation.isolation.successfulMessage', { defaultMessage: 'Host Isolation on {hostName} successfully submitted', values: { hostName }, }); + +export const GET_UNISOLATION_SUCCESS_MESSAGE = (hostName: string) => + i18n.translate('xpack.securitySolution.endpoint.hostIsolation.unisolate.successfulMessage', { + defaultMessage: 'Host Unisolation on {hostName} successfully submitted', + values: { hostName }, + }); + +export const ISOLATE = i18n.translate('xpack.securitySolution.endpoint.hostisolation.isolate', { + defaultMessage: 'isolate', +}); + +export const UNISOLATE = i18n.translate('xpack.securitySolution.endpoint.hostisolation.unisolate', { + defaultMessage: 'unisolate', +}); + +export const NOT_ISOLATED = i18n.translate( + 'xpack.securitySolution.endpoint.hostIsolation.notIsolated', + { + defaultMessage: 'not isolated', + } +); + +export const ISOLATED = i18n.translate('xpack.securitySolution.endpoint.hostIsolation.isolated', { + defaultMessage: 'isolated', +}); diff --git a/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/unisolate_form.tsx b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/unisolate_form.tsx new file mode 100644 index 00000000000000..98006524844c4d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/endpoint/host_isolation/unisolate_form.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, { ChangeEventHandler, memo, useCallback } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiText, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CANCEL, COMMENT, COMMENT_PLACEHOLDER, CONFIRM, UNISOLATE, ISOLATED } from './translations'; +import { EndpointIsolatedFormProps } from './isolate_form'; + +export const EndpointUnisolateForm = memo( + ({ hostName, onCancel, onConfirm, onChange, comment = '', messageAppend, isLoading = false }) => { + const handleCommentChange: ChangeEventHandler = useCallback( + (event) => { + onChange({ comment: event.target.value }); + }, + [onChange] + ); + + return ( + <> + +

+ {hostName}, + isolated: {ISOLATED}, + unisolate: {UNISOLATE}, + }} + />{' '} + {messageAppend} +

+
+ + + + +

{COMMENT}

+
+ + + + + + + + {CANCEL} + + + + + {CONFIRM} + + + + + ); + } +); + +EndpointUnisolateForm.displayName = 'EndpointUnisolateForm'; diff --git a/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_isolate_privileges.ts b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_isolate_privileges.ts new file mode 100644 index 00000000000000..23ef6d586adc54 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/endpoint/use_isolate_privileges.ts @@ -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 { useEffect, useState } from 'react'; +import { userCanIsolate } from '../../../../common/endpoint/actions'; +import { useKibana } from '../../lib/kibana'; +import { useLicense } from '../use_license'; + +interface IsolationPriviledgesStatus { + isLoading: boolean; + isAllowed: boolean; +} + +/* + * Host isolation requires superuser privileges and at least a platinum license + */ +export const useIsolationPrivileges = (): IsolationPriviledgesStatus => { + const [isLoading, setIsLoading] = useState(false); + const [canIsolate, setCanIsolate] = useState(false); + + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const services = useKibana().services; + + useEffect(() => { + setIsLoading(true); + const user = services.security.authc.getCurrentUser(); + if (user) { + user.then((authenticatedUser) => { + setCanIsolate(userCanIsolate(authenticatedUser.roles)); + setIsLoading(false); + }); + } + }, [services.security.authc]); + + return { isLoading, isAllowed: canIsolate && isPlatinumPlus ? true : false }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx index e6fd3a8459f764..bb1585b5392bd5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/index.tsx @@ -5,33 +5,27 @@ * 2.0. */ -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useMemo } from 'react'; import { find } from 'lodash/fp'; -import { EuiText, EuiSpacer } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation'; -import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations'; import { Maybe } from '../../../../../observability/common/typings'; import { useCasesFromAlerts } from '../../containers/detection_engine/alerts/use_cases_from_alerts'; import { CaseDetailsLink } from '../../../common/components/links'; import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; -import { - EndpointIsolatedFormProps, - EndpointIsolateForm, - EndpointIsolateSuccess, -} from '../../../common/components/endpoint/host_isolation'; +import { IsolateHost } from './isolate'; +import { UnisolateHost } from './unisolate'; export const HostIsolationPanel = React.memo( ({ details, cancelCallback, + isolateAction, }: { details: Maybe; cancelCallback: () => void; + isolateAction: string; }) => { - const [comment, setComment] = useState(''); - const [isIsolated, setIsIsolated] = useState(false); - const agentId = useMemo(() => { const findAgentId = find({ category: 'agent', field: 'agent.id' }, details)?.values; return findAgentId ? findAgentId[0] : ''; @@ -54,25 +48,15 @@ export const HostIsolationPanel = React.memo( }, [details]); const { caseIds } = useCasesFromAlerts({ alertId }); - const { loading, isolateHost } = useHostIsolation({ agentId, comment, caseIds }); - - const confirmHostIsolation = useCallback(async () => { - const hostIsolated = await isolateHost(); - setIsIsolated(hostIsolated); - }, [isolateHost]); - - const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); - const handleIsolateFormChange: EndpointIsolatedFormProps['onChange'] = useCallback( - ({ comment: newComment }) => setComment(newComment), - [] - ); + // Cases related components to be used in both isolate and unisolate actions from the alert details flyout entry point + const caseCount: number = useMemo(() => caseIds.length, [caseIds]); const casesList = useMemo( () => caseIds.map((id, index) => { return ( -
  • +
  • caseIds.length, [caseIds]); - - const hostIsolated = useMemo(() => { - return ( - <> - - 0 && ( - <> - -

    - -

    -
    - -
      {casesList}
    -
    - - ) - } - /> - - ); - }, [backToAlertDetails, hostName, caseCount, casesList]); - - const hostNotIsolated = useMemo(() => { - return ( - <> - - - {caseCount} - {CASES_ASSOCIATED_WITH_ALERT(caseCount)} - {alertRule} - - ), - }} - /> - } - /> - - ); - }, [ - hostName, - backToAlertDetails, - confirmHostIsolation, - handleIsolateFormChange, - comment, - loading, - caseCount, - alertRule, - ]); - - return isIsolated ? hostIsolated : hostNotIsolated; + const associatedCases = useMemo(() => { + if (caseCount > 0) { + return ( + <> + +

    + +

    +
    + +
      {casesList}
    +
    + + ); + } + }, [caseCount, casesList]); + + return isolateAction === 'isolateHost' ? ( + + ) : ( + + ); } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx new file mode 100644 index 00000000000000..10c9082976a0e5 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/isolate.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, useCallback, ReactNode } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHostIsolation } from '../../containers/detection_engine/alerts/use_host_isolation'; +import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations'; +import { + EndpointIsolatedFormProps, + EndpointIsolateForm, + EndpointIsolateSuccess, +} from '../../../common/components/endpoint/host_isolation'; + +export const IsolateHost = React.memo( + ({ + agentId, + hostName, + alertRule, + cases, + caseIds, + cancelCallback, + }: { + agentId: string; + hostName: string; + alertRule: string; + cases: ReactNode; + caseIds: string[]; + cancelCallback: () => void; + }) => { + const [comment, setComment] = useState(''); + const [isIsolated, setIsIsolated] = useState(false); + + const { loading, isolateHost } = useHostIsolation({ agentId, comment, caseIds }); + + const confirmHostIsolation = useCallback(async () => { + const hostIsolated = await isolateHost(); + setIsIsolated(hostIsolated); + }, [isolateHost]); + + const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); + + const handleIsolateFormChange: EndpointIsolatedFormProps['onChange'] = useCallback( + ({ comment: newComment }) => setComment(newComment), + [] + ); + + const caseCount: number = useMemo(() => caseIds.length, [caseIds]); + + const hostIsolatedSuccess = useMemo(() => { + return ( + <> + + + + ); + }, [backToAlertDetails, hostName, cases]); + + const hostNotIsolated = useMemo(() => { + return ( + <> + + + {caseCount} + {CASES_ASSOCIATED_WITH_ALERT(caseCount)} + {alertRule} + + ), + }} + /> + } + /> + + ); + }, [ + hostName, + backToAlertDetails, + confirmHostIsolation, + handleIsolateFormChange, + comment, + loading, + caseCount, + alertRule, + ]); + + return isIsolated ? hostIsolatedSuccess : hostNotIsolated; + } +); + +IsolateHost.displayName = 'IsolateHost'; diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx index 36f2553e1b9272..a10ad901441ea5 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/take_action_dropdown.tsx @@ -7,30 +7,33 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuItem, EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; -import { ISOLATE_HOST } from './translations'; +import { ISOLATE_HOST, UNISOLATE_HOST } from './translations'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; +import { useHostIsolationStatus } from '../../containers/detection_engine/alerts/use_host_isolation_status'; export const TakeActionDropdown = React.memo( - ({ onChange }: { onChange: (action: 'isolateHost') => void }) => { + ({ + onChange, + agentId, + }: { + onChange: (action: 'isolateHost' | 'unisolateHost') => void; + agentId: string; + }) => { + const { loading, isIsolated: isolationStatus } = useHostIsolationStatus({ agentId }); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const closePopoverHandler = useCallback(() => { setIsPopoverOpen(false); }, []); - const takeActionItems = useMemo(() => { - return [ - { - setIsPopoverOpen(false); - onChange('isolateHost'); - }} - > - {ISOLATE_HOST} - , - ]; - }, [onChange]); + const isolateHostHandler = useCallback(() => { + setIsPopoverOpen(false); + if (isolationStatus === false) { + onChange('isolateHost'); + } else { + onChange('unisolateHost'); + } + }, [onChange, isolationStatus]); const takeActionButton = useMemo(() => { return ( @@ -38,6 +41,7 @@ export const TakeActionDropdown = React.memo( iconSide="right" fill iconType="arrowDown" + disabled={loading} onClick={() => { setIsPopoverOpen(!isPopoverOpen); }} @@ -45,7 +49,7 @@ export const TakeActionDropdown = React.memo( {TAKE_ACTION} ); - }, [isPopoverOpen]); + }, [isPopoverOpen, loading]); return ( - + + {isolationStatus === false ? ( + + {ISOLATE_HOST} + + ) : ( + + {UNISOLATE_HOST} + + )} + ); } diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts index 027a97cc3846ef..449a09b932cd31 100644 --- a/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/translations.ts @@ -8,12 +8,19 @@ import { i18n } from '@kbn/i18n'; export const ISOLATE_HOST = i18n.translate( - 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.isolateHost', + 'xpack.securitySolution.endpoint.hostIsolation.isolateHost', { defaultMessage: 'Isolate host', } ); +export const UNISOLATE_HOST = i18n.translate( + 'xpack.securitySolution.endpoint.hostIsolation.unisolateHost', + { + defaultMessage: 'Unisolate host', + } +); + export const CASES_ASSOCIATED_WITH_ALERT = (caseCount: number): string => i18n.translate( 'xpack.securitySolution.endpoint.hostIsolation.isolateHost.casesAssociatedWithAlert', diff --git a/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx new file mode 100644 index 00000000000000..74149f2a692d3c --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/components/host_isolation/unisolate.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo, useState, useCallback, ReactNode } from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CASES_ASSOCIATED_WITH_ALERT, RETURN_TO_ALERT_DETAILS } from './translations'; +import { + EndpointIsolatedFormProps, + EndpointIsolateSuccess, + EndpointUnisolateForm, +} from '../../../common/components/endpoint/host_isolation'; +import { useHostUnisolation } from '../../containers/detection_engine/alerts/use_host_unisolation'; + +export const UnisolateHost = React.memo( + ({ + agentId, + hostName, + alertRule, + cases, + caseIds, + cancelCallback, + }: { + agentId: string; + hostName: string; + alertRule: string; + cases: ReactNode; + caseIds: string[]; + cancelCallback: () => void; + }) => { + const [comment, setComment] = useState(''); + const [isUnIsolated, setIsUnIsolated] = useState(false); + + const { loading, unIsolateHost } = useHostUnisolation({ agentId, comment, caseIds }); + + const confirmHostUnIsolation = useCallback(async () => { + const hostIsolated = await unIsolateHost(); + setIsUnIsolated(hostIsolated); + }, [unIsolateHost]); + + const backToAlertDetails = useCallback(() => cancelCallback(), [cancelCallback]); + + const handleIsolateFormChange: EndpointIsolatedFormProps['onChange'] = useCallback( + ({ comment: newComment }) => setComment(newComment), + [] + ); + + const caseCount: number = useMemo(() => caseIds.length, [caseIds]); + + const hostUnisolatedSuccess = useMemo(() => { + return ( + <> + + + + ); + }, [backToAlertDetails, hostName, cases]); + + const hostNotUnisolated = useMemo(() => { + return ( + <> + + + {caseCount} + {CASES_ASSOCIATED_WITH_ALERT(caseCount)} + {alertRule} + + ), + }} + /> + } + /> + + ); + }, [ + hostName, + backToAlertDetails, + confirmHostUnIsolation, + handleIsolateFormChange, + comment, + loading, + caseCount, + alertRule, + ]); + + return isUnIsolated ? hostUnisolatedSuccess : hostNotUnisolated; + } +); + +UnisolateHost.displayName = 'UnisolateHost'; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts index 28a6076421e837..65185b4d05135b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/api.ts @@ -7,13 +7,14 @@ import { UpdateDocumentByQueryResponse } from 'elasticsearch'; import { getCasesFromAlertsUrl } from '../../../../../../cases/common'; -import { HostIsolationResponse } from '../../../../../common/endpoint/types'; +import { HostIsolationResponse, HostMetadataInfo } from '../../../../../common/endpoint/types'; import { DETECTION_ENGINE_QUERY_SIGNALS_URL, DETECTION_ENGINE_SIGNALS_STATUS_URL, DETECTION_ENGINE_INDEX_URL, DETECTION_ENGINE_PRIVILEGES_URL, } from '../../../../../common/constants'; +import { HOST_METADATA_GET_API } from '../../../../../common/endpoint/constants'; import { KibanaServices } from '../../../../common/lib/kibana'; import { BasicSignals, @@ -24,7 +25,8 @@ import { UpdateAlertStatusProps, CasesFromAlertsResponse, } from './types'; -import { isolateHost } from '../../../../common/lib/host_isolation'; +import { resolvePathVariables } from '../../../../management/pages/trusted_apps/service/utils'; +import { isolateHost, unIsolateHost } from '../../../../common/lib/host_isolation'; /** * Fetch Alerts by providing a query @@ -130,6 +132,30 @@ export const createHostIsolation = async ({ case_ids: caseIds, }); +/** + * Unisolate a host + * + * @param agent id + * @param optional comment for the unisolation action + * @param optional case ids if associated with an alert on the host + * + * @throws An error if response is not OK + */ +export const createHostUnIsolation = async ({ + agentId, + comment = '', + caseIds, +}: { + agentId: string; + comment?: string; + caseIds?: string[]; +}): Promise => + unIsolateHost({ + agent_ids: [agentId], + comment, + case_ids: caseIds, + }); + /** * Get list of associated case ids from alert id * @@ -143,3 +169,18 @@ export const getCaseIdsFromAlertId = async ({ KibanaServices.get().http.fetch(getCasesFromAlertsUrl(alertId), { method: 'get', }); + +/** + * Get Host metadata + * + * @param host id + */ +export const getHostMetadata = async ({ + agentId, +}: { + agentId: string; +}): Promise => + KibanaServices.get().http.fetch( + resolvePathVariables(HOST_METADATA_GET_API, { id: agentId }), + { method: 'get' } + ); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts index ed6a22375a7769..9e4497f2f096bb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/translations.ts @@ -37,3 +37,8 @@ export const CASES_FROM_ALERTS_FAILURE = i18n.translate( 'xpack.securitySolution.endpoint.hostIsolation.casesFromAlerts.title', { defaultMessage: 'Failed to find associated cases' } ); + +export const ISOLATION_STATUS_FAILURE = i18n.translate( + 'xpack.securitySolution.endpoint.hostIsolation.isolationStatus.title', + { defaultMessage: 'Failed to retrieve current isolation status' } +); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.tsx new file mode 100644 index 00000000000000..adc6d3a6b054b8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_isolation_status.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 { isEmpty } from 'lodash'; +import { useEffect, useState } from 'react'; +import { Maybe } from '../../../../../../observability/common/typings'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { getHostMetadata } from './api'; +import { ISOLATION_STATUS_FAILURE } from './translations'; + +interface HostIsolationStatusResponse { + loading: boolean; + isIsolated: Maybe; +} + +/* + * Retrieves the current isolation status of a host */ +export const useHostIsolationStatus = ({ + agentId, +}: { + agentId: string; +}): HostIsolationStatusResponse => { + const [isIsolated, setIsIsolated] = useState>(); + const [loading, setLoading] = useState(false); + + const { addError } = useAppToasts(); + + useEffect(() => { + // isMounted tracks if a component is mounted before changing state + let isMounted = true; + const fetchData = async () => { + try { + const metadataResponse = await getHostMetadata({ agentId }); + if (isMounted) { + setIsIsolated(Boolean(metadataResponse.metadata.Endpoint.state.isolation)); + } + } catch (error) { + addError(error.message, { title: ISOLATION_STATUS_FAILURE }); + } + if (isMounted) { + setLoading(false); + } + }; + + setLoading((prevState) => { + if (prevState) { + return prevState; + } + if (!isEmpty(agentId)) { + fetchData(); + } + return true; + }); + return () => { + // updates to show component is unmounted + isMounted = false; + }; + }, [addError, agentId]); + return { loading, isIsolated }; +}; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_unisolation.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_unisolation.tsx new file mode 100644 index 00000000000000..1a0ecb0d158781 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_host_unisolation.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, useState } from 'react'; +import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; +import { HOST_ISOLATION_FAILURE } from './translations'; +import { createHostUnIsolation } from './api'; + +interface HostUnisolationStatus { + loading: boolean; + unIsolateHost: () => Promise; +} + +interface UseHostIsolationProps { + agentId: string; + comment: string; + caseIds?: string[]; +} + +export const useHostUnisolation = ({ + agentId, + comment, + caseIds, +}: UseHostIsolationProps): HostUnisolationStatus => { + const [loading, setLoading] = useState(false); + const { addError } = useAppToasts(); + + const unIsolateHost = useCallback(async () => { + try { + setLoading(true); + const isolationStatus = await createHostUnIsolation({ agentId, comment, caseIds }); + setLoading(false); + return isolationStatus.action ? true : false; + } catch (error) { + setLoading(false); + addError(error.message, { title: HOST_ISOLATION_FAILURE }); + return false; + } + }, [agentId, comment, caseIds, addError]); + return { loading, unIsolateHost }; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index ffde8b09317521..9db9932dd4387b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -36,8 +36,13 @@ import { sendGetFleetAgentsWithEndpoint, } from '../../policy/store/services/ingest'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../fleet/common'; -import { metadataCurrentIndexPattern } from '../../../../../common/endpoint/constants'; +import { + HOST_METADATA_GET_API, + HOST_METADATA_LIST_API, + metadataCurrentIndexPattern, +} from '../../../../../common/endpoint/constants'; import { IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; +import { resolvePathVariables } from '../../trusted_apps/service/utils'; import { createFailedResourceState, createLoadedResourceState, @@ -95,7 +100,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory('/api/endpoint/metadata', { + endpointResponse = await coreStart.http.post(HOST_METADATA_LIST_API, { body: JSON.stringify({ paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], filters: { kql: decodedQuery.query }, @@ -244,7 +249,7 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory( - `/api/endpoint/metadata/${selectedEndpoint}` + resolvePathVariables(HOST_METADATA_GET_API, { id: selectedEndpoint as string }) ); dispatch({ type: 'serverReturnedEndpointDetails', @@ -426,7 +431,7 @@ const getAgentAndPoliciesForEndpointsList = async ( const endpointsTotal = async (http: HttpStart): Promise => { try { return ( - await http.post('/api/endpoint/metadata', { + await http.post(HOST_METADATA_LIST_API, { body: JSON.stringify({ paging_properties: [{ page_index: 0 }, { page_size: 1 }], }), diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index de4795869f4b4b..341397da09e1d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -26,9 +26,13 @@ import { useTimelineEventsDetails } from '../../../containers/details'; import { TimelineTabs } from '../../../../../common/types/timeline'; import { HostIsolationPanel } from '../../../../detections/components/host_isolation'; import { TakeActionDropdown } from '../../../../detections/components/host_isolation/take_action_dropdown'; -import { ISOLATE_HOST } from '../../../../detections/components/host_isolation/translations'; +import { + ISOLATE_HOST, + UNISOLATE_HOST, +} from '../../../../detections/components/host_isolation/translations'; import { ALERT_DETAILS } from './translations'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; +import { useIsolationPrivileges } from '../../../../common/hooks/endpoint/use_isolate_privileges'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflow { @@ -74,13 +78,17 @@ const EventDetailsPanelComponent: React.FC = ({ const [isHostIsolationPanelOpen, setIsHostIsolationPanel] = useState(false); + const [isolateAction, setIsolateAction] = useState('isolateHost'); + const showAlertDetails = useCallback(() => { setIsHostIsolationPanel(false); }, []); + const { isAllowed: isIsolationAllowed } = useIsolationPrivileges(); const showHostIsolationPanel = useCallback((action) => { - if (action === 'isolateHost') { + if (action === 'isolateHost' || action === 'unisolateHost') { setIsHostIsolationPanel(true); + setIsolateAction(action); } }, []); @@ -91,6 +99,11 @@ const EventDetailsPanelComponent: React.FC = ({ return findEndpointAlert ? findEndpointAlert[0] === 'endpoint' : false; }, [detailsData]); + const agentId = useMemo(() => { + const findAgentId = find({ category: 'agent', field: 'agent.id' }, detailsData)?.values; + return findAgentId ? findAgentId[0] : ''; + }, [detailsData]); + const backToAlertDetailsLink = useMemo(() => { return ( <> @@ -105,11 +118,11 @@ const EventDetailsPanelComponent: React.FC = ({ -

    {ISOLATE_HOST}

    +

    {isolateAction === 'isolateHost' ? ISOLATE_HOST : UNISOLATE_HOST}

    ); - }, [showAlertDetails]); + }, [showAlertDetails, isolateAction]); if (!expandedEvent?.eventId) { return null; @@ -126,7 +139,11 @@ const EventDetailsPanelComponent: React.FC = ({ {isHostIsolationPanelOpen ? ( - + ) : ( = ({ /> )} - {isHostIsolationEnabled && isEndpointAlert && isHostIsolationPanelOpen === false && ( - - - - - - - - - - )} + {isIsolationAllowed && + isHostIsolationEnabled && + isEndpointAlert && + isHostIsolationPanelOpen === false && ( + + + + + + + + + + )} ) : ( <> diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 09d26a20f10956..68420411284651 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -20,14 +20,7 @@ import { } from '../../../types'; import { getAgentIDsForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; - -export const userCanIsolate = (roles: readonly string[] | undefined): boolean => { - // only superusers can write to the fleet index (or look up endpoint data to convert endp ID to agent ID) - if (!roles || roles.length === 0) { - return false; - } - return roles.includes('superuser'); -}; +import { userCanIsolate } from '../../../../common/endpoint/actions'; /** * Registers the Host-(un-)isolation routes diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 104383f3986462..98610c2e84c02c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -17,7 +17,7 @@ import { import { HostInfo, HostMetadata, - HostMetaDataInfo, + HostMetadataInfo, HostResultList, HostStatus, MetadataQueryStrategyVersions, @@ -182,7 +182,7 @@ export async function getHostMetaData( metadataRequestContext: MetadataRequestContext, id: string, queryStrategyVersion?: MetadataQueryStrategyVersions -): Promise { +): Promise { if ( !metadataRequestContext.esClient && !metadataRequestContext.requestHandlerContext?.core.elasticsearch.client diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 72db0be6ce278f..aa37a0dc1f6270 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -239,7 +239,12 @@ export class Plugin implements IPlugin void; export interface UsageData { - detections: DetectionsUsage; endpoints: EndpointUsage | {}; detectionMetrics: {}; } @@ -40,55 +34,10 @@ export const registerCollector: RegisterCollector = ({ if (!usageCollection) { return; } + const collector = usageCollection.makeUsageCollector({ type: 'security_solution', schema: { - detections: { - detection_rules: { - custom: { - enabled: { - type: 'long', - _meta: { description: 'The number of custom detection rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'The number of custom detection rules disabled' }, - }, - }, - elastic: { - enabled: { - type: 'long', - _meta: { description: 'The number of elastic prebuilt detection rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'The number of elastic prebuilt detection rules disabled' }, - }, - }, - }, - ml_jobs: { - custom: { - enabled: { - type: 'long', - _meta: { description: 'The number of custom ML jobs rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'The number of custom ML jobs rules disabled' }, - }, - }, - elastic: { - enabled: { - type: 'long', - _meta: { description: 'The number of elastic provided ML jobs rules enabled' }, - }, - disabled: { - type: 'long', - _meta: { description: 'The number of elastic provided ML jobs rules disabled' }, - }, - }, - }, - }, detectionMetrics: { detection_rules: { detection_rule_usage: { @@ -248,172 +197,199 @@ export const registerCollector: RegisterCollector = ({ }, }, ml_jobs: { - type: 'array', - items: { - job_id: { - type: 'keyword', - _meta: { description: 'Identifier for the anomaly detection job' }, - }, - open_time: { - type: 'keyword', - _meta: { - description: 'For open jobs only, the elapsed time for which the job has been open', - }, - }, - create_time: { - type: 'keyword', - _meta: { description: 'The time the job was created' }, - }, - finished_time: { - type: 'keyword', - _meta: { - description: 'If the job closed or failed, this is the time the job finished', - }, - }, - state: { - type: 'keyword', - _meta: { description: 'The status of the anomaly detection job' }, - }, - data_counts: { - bucket_count: { + ml_job_usage: { + custom: { + enabled: { type: 'long', - _meta: { description: 'The number of buckets processed' }, + _meta: { description: 'The number of custom ML jobs rules enabled' }, }, - empty_bucket_count: { + disabled: { type: 'long', - _meta: { description: 'The number of buckets which did not contain any data' }, + _meta: { description: 'The number of custom ML jobs rules disabled' }, }, - input_bytes: { + }, + elastic: { + enabled: { type: 'long', - _meta: { - description: - 'The number of bytes of input data posted to the anomaly detection job', - }, + _meta: { description: 'The number of elastic provided ML jobs rules enabled' }, }, - input_record_count: { + disabled: { type: 'long', - _meta: { - description: 'The number of input documents posted to the anomaly detection job', - }, + _meta: { description: 'The number of elastic provided ML jobs rules disabled' }, }, - last_data_time: { - type: 'long', - _meta: { - description: - 'The timestamp at which data was last analyzed, according to server time', - }, + }, + }, + ml_job_metrics: { + type: 'array', + items: { + job_id: { + type: 'keyword', + _meta: { description: 'Identifier for the anomaly detection job' }, }, - processed_record_count: { - type: 'long', + open_time: { + type: 'keyword', _meta: { description: - 'The number of input documents that have been processed by the anomaly detection job', + 'For open jobs only, the elapsed time for which the job has been open', }, }, - }, - model_size_stats: { - bucket_allocation_failures_count: { - type: 'long', + create_time: { + type: 'keyword', + _meta: { description: 'The time the job was created' }, + }, + finished_time: { + type: 'keyword', _meta: { - description: - 'The number of buckets for which new entities in incoming data were not processed due to insufficient model memory', + description: 'If the job closed or failed, this is the time the job finished', }, }, - model_bytes: { - type: 'long', - _meta: { description: 'The number of bytes of memory used by the models' }, + state: { + type: 'keyword', + _meta: { description: 'The status of the anomaly detection job' }, }, - model_bytes_exceeded: { - type: 'long', - _meta: { - description: - 'The number of bytes over the high limit for memory usage at the last allocation failure', + data_counts: { + bucket_count: { + type: 'long', + _meta: { description: 'The number of buckets processed' }, }, - }, - model_bytes_memory_limit: { - type: 'long', - _meta: { - description: - 'The upper limit for model memory usage, checked on increasing values', + empty_bucket_count: { + type: 'long', + _meta: { description: 'The number of buckets which did not contain any data' }, }, - }, - peak_model_bytes: { - type: 'long', - _meta: { - description: 'The peak number of bytes of memory ever used by the models', + input_bytes: { + type: 'long', + _meta: { + description: + 'The number of bytes of input data posted to the anomaly detection job', + }, }, - }, - }, - timing_stats: { - bucket_count: { - type: 'long', - _meta: { description: 'The number of buckets processed' }, - }, - exponential_average_bucket_processing_time_ms: { - type: 'long', - _meta: { - description: - 'Exponential moving average of all bucket processing times, in milliseconds', + input_record_count: { + type: 'long', + _meta: { + description: + 'The number of input documents posted to the anomaly detection job', + }, }, - }, - exponential_average_bucket_processing_time_per_hour_ms: { - type: 'long', - _meta: { - description: - 'Exponentially-weighted moving average of bucket processing times calculated in a 1 hour time window, in milliseconds', + last_data_time: { + type: 'long', + _meta: { + description: + 'The timestamp at which data was last analyzed, according to server time', + }, }, - }, - maximum_bucket_processing_time_ms: { - type: 'long', - _meta: { - description: 'Maximum among all bucket processing times, in milliseconds', + processed_record_count: { + type: 'long', + _meta: { + description: + 'The number of input documents that have been processed by the anomaly detection job', + }, }, }, - minimum_bucket_processing_time_ms: { - type: 'long', - _meta: { - description: 'Minimum among all bucket processing times, in milliseconds', + model_size_stats: { + bucket_allocation_failures_count: { + type: 'long', + _meta: { + description: + 'The number of buckets for which new entities in incoming data were not processed due to insufficient model memory', + }, }, - }, - total_bucket_processing_time_ms: { - type: 'long', - _meta: { description: 'Sum of all bucket processing times, in milliseconds' }, - }, - }, - datafeed: { - datafeed_id: { - type: 'keyword', - _meta: { - description: 'A numerical character string that uniquely identifies the datafeed', + model_bytes: { + type: 'long', + _meta: { description: 'The number of bytes of memory used by the models' }, }, - }, - state: { - type: 'keyword', - _meta: { description: 'The status of the datafeed' }, - }, - timing_stats: { - average_search_time_per_bucket_ms: { + model_bytes_exceeded: { + type: 'long', + _meta: { + description: + 'The number of bytes over the high limit for memory usage at the last allocation failure', + }, + }, + model_bytes_memory_limit: { + type: 'long', + _meta: { + description: + 'The upper limit for model memory usage, checked on increasing values', + }, + }, + peak_model_bytes: { type: 'long', - _meta: { description: 'The average search time per bucket, in milliseconds' }, + _meta: { + description: 'The peak number of bytes of memory ever used by the models', + }, }, + }, + timing_stats: { bucket_count: { type: 'long', _meta: { description: 'The number of buckets processed' }, }, - exponential_average_search_time_per_hour_ms: { + exponential_average_bucket_processing_time_ms: { + type: 'long', + _meta: { + description: + 'Exponential moving average of all bucket processing times, in milliseconds', + }, + }, + exponential_average_bucket_processing_time_per_hour_ms: { type: 'long', _meta: { - description: 'The exponential average search time per hour, in milliseconds', + description: + 'Exponentially-weighted moving average of bucket processing times calculated in a 1 hour time window, in milliseconds', }, }, - search_count: { + maximum_bucket_processing_time_ms: { type: 'long', - _meta: { description: 'The number of searches run by the datafeed' }, + _meta: { + description: 'Maximum among all bucket processing times, in milliseconds', + }, + }, + minimum_bucket_processing_time_ms: { + type: 'long', + _meta: { + description: 'Minimum among all bucket processing times, in milliseconds', + }, }, - total_search_time_ms: { + total_bucket_processing_time_ms: { type: 'long', + _meta: { description: 'Sum of all bucket processing times, in milliseconds' }, + }, + }, + datafeed: { + datafeed_id: { + type: 'keyword', _meta: { - description: 'The total time the datafeed spent searching, in milliseconds', + description: + 'A numerical character string that uniquely identifies the datafeed', + }, + }, + state: { + type: 'keyword', + _meta: { description: 'The status of the datafeed' }, + }, + timing_stats: { + average_search_time_per_bucket_ms: { + type: 'long', + _meta: { description: 'The average search time per bucket, in milliseconds' }, + }, + bucket_count: { + type: 'long', + _meta: { description: 'The number of buckets processed' }, + }, + exponential_average_search_time_per_hour_ms: { + type: 'long', + _meta: { + description: 'The exponential average search time per hour, in milliseconds', + }, + }, + search_count: { + type: 'long', + _meta: { description: 'The number of searches run by the datafeed' }, + }, + total_search_time_ms: { + type: 'long', + _meta: { + description: 'The total time the datafeed spent searching, in milliseconds', + }, }, }, }, @@ -476,14 +452,12 @@ export const registerCollector: RegisterCollector = ({ fetch: async ({ esClient }: CollectorFetchContext): Promise => { const internalSavedObjectsClient = await getInternalSavedObjectsClient(core); const savedObjectsClient = (internalSavedObjectsClient as unknown) as SavedObjectsClientContract; - const [detections, detectionMetrics, endpoints] = await Promise.allSettled([ - fetchDetectionsUsage(kibanaIndex, esClient, ml, savedObjectsClient), + const [detectionMetrics, endpoints] = await Promise.allSettled([ fetchDetectionsMetrics(kibanaIndex, signalsIndex, esClient, ml, savedObjectsClient), getEndpointTelemetryFromFleet(savedObjectsClient, endpointAppContext, esClient), ]); return { - detections: detections.status === 'fulfilled' ? detections.value : defaultDetectionsUsage, detectionMetrics: detectionMetrics.status === 'fulfilled' ? detectionMetrics.value : {}, endpoints: endpoints.status === 'fulfilled' ? endpoints.value : {}, }; diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_ml_helpers.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_ml_helpers.test.ts new file mode 100644 index 00000000000000..3ca0faeca7d361 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/detections/detection_ml_helpers.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { initialMlJobsUsage, updateMlJobsUsage } from './detection_ml_helpers'; + +describe('Security Machine Learning usage metrics', () => { + describe('Updates metrics with job information', () => { + it('Should update ML total for elastic rules', async () => { + const initialUsage = initialMlJobsUsage; + const isElastic = true; + const isEnabled = true; + + const updatedUsage = updateMlJobsUsage({ isElastic, isEnabled }, initialUsage); + + expect(updatedUsage).toEqual( + expect.objectContaining({ + custom: { + disabled: 0, + enabled: 0, + }, + elastic: { + disabled: 0, + enabled: 1, + }, + }) + ); + }); + + it('Should update ML total for custom rules', async () => { + const initialUsage = initialMlJobsUsage; + const isElastic = false; + const isEnabled = true; + + const updatedUsage = updateMlJobsUsage({ isElastic, isEnabled }, initialUsage); + + expect(updatedUsage).toEqual( + expect.objectContaining({ + custom: { + disabled: 0, + enabled: 1, + }, + elastic: { + disabled: 0, + enabled: 0, + }, + }) + ); + }); + + it('Should update ML total for both elastic and custom rules', async () => { + const initialUsage = initialMlJobsUsage; + + let updatedUsage = updateMlJobsUsage({ isElastic: true, isEnabled: true }, initialUsage); + updatedUsage = updateMlJobsUsage({ isElastic: false, isEnabled: true }, updatedUsage); + + expect(updatedUsage).toEqual( + expect.objectContaining({ + custom: { + disabled: 0, + enabled: 1, + }, + elastic: { + disabled: 0, + enabled: 1, + }, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_ml_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_ml_helpers.ts new file mode 100644 index 00000000000000..f9571b98c9d594 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/detections/detection_ml_helpers.ts @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server'; +import { DatafeedStats, Job, MlPluginSetup } from '../../../../ml/server'; +import { isJobStarted } from '../../../common/machine_learning/helpers'; +import { isSecurityJob } from '../../../common/machine_learning/is_security_job'; +import { DetectionsMetric, MlJobMetric, MlJobsUsage, MlJobUsage } from './types'; + +/** + * Default ml job usage count + */ +export const initialMlJobsUsage: MlJobsUsage = { + custom: { + enabled: 0, + disabled: 0, + }, + elastic: { + enabled: 0, + disabled: 0, + }, +}; + +export const updateMlJobsUsage = (jobMetric: DetectionsMetric, usage: MlJobsUsage): MlJobsUsage => { + const { isEnabled, isElastic } = jobMetric; + if (isEnabled && isElastic) { + return { + ...usage, + elastic: { + ...usage.elastic, + enabled: usage.elastic.enabled + 1, + }, + }; + } else if (!isEnabled && isElastic) { + return { + ...usage, + elastic: { + ...usage.elastic, + disabled: usage.elastic.disabled + 1, + }, + }; + } else if (isEnabled && !isElastic) { + return { + ...usage, + custom: { + ...usage.custom, + enabled: usage.custom.enabled + 1, + }, + }; + } else if (!isEnabled && !isElastic) { + return { + ...usage, + custom: { + ...usage.custom, + disabled: usage.custom.disabled + 1, + }, + }; + } else { + return usage; + } +}; + +export const getMlJobMetrics = async ( + ml: MlPluginSetup | undefined, + savedObjectClient: SavedObjectsClientContract +): Promise => { + let jobsUsage: MlJobsUsage = initialMlJobsUsage; + + if (ml) { + try { + const fakeRequest = { headers: {} } as KibanaRequest; + + const modules = await ml.modulesProvider(fakeRequest, savedObjectClient).listModules(); + const moduleJobs = modules.flatMap((module) => module.jobs); + const jobs = await ml.jobServiceProvider(fakeRequest, savedObjectClient).jobsSummary(); + + jobsUsage = jobs.filter(isSecurityJob).reduce((usage, job) => { + const isElastic = moduleJobs.some((moduleJob) => moduleJob.id === job.id); + const isEnabled = isJobStarted(job.jobState, job.datafeedState); + + return updateMlJobsUsage({ isElastic, isEnabled }, usage); + }, initialMlJobsUsage); + + const jobsType = 'security'; + const securityJobStats = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .jobStats(jobsType); + + const jobDetails = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .jobs(jobsType); + + const jobDetailsCache = new Map(); + jobDetails.jobs.forEach((detail) => jobDetailsCache.set(detail.job_id, detail)); + + const datafeedStats = await ml + .anomalyDetectorsProvider(fakeRequest, savedObjectClient) + .datafeedStats(); + + const datafeedStatsCache = new Map(); + datafeedStats.datafeeds.forEach((datafeedStat) => + datafeedStatsCache.set(`${datafeedStat.datafeed_id}`, datafeedStat) + ); + + const jobMetrics: MlJobMetric[] = securityJobStats.jobs.map((stat) => { + const jobId = stat.job_id; + const jobDetail = jobDetailsCache.get(stat.job_id); + const datafeed = datafeedStatsCache.get(`datafeed-${jobId}`); + + return { + job_id: jobId, + open_time: stat.open_time, + create_time: jobDetail?.create_time, + finished_time: jobDetail?.finished_time, + state: stat.state, + data_counts: { + bucket_count: stat.data_counts.bucket_count, + empty_bucket_count: stat.data_counts.empty_bucket_count, + input_bytes: stat.data_counts.input_bytes, + input_record_count: stat.data_counts.input_record_count, + last_data_time: stat.data_counts.last_data_time, + processed_record_count: stat.data_counts.processed_record_count, + }, + model_size_stats: { + bucket_allocation_failures_count: + stat.model_size_stats.bucket_allocation_failures_count, + memory_status: stat.model_size_stats.memory_status, + model_bytes: stat.model_size_stats.model_bytes, + model_bytes_exceeded: stat.model_size_stats.model_bytes_exceeded, + model_bytes_memory_limit: stat.model_size_stats.model_bytes_memory_limit, + peak_model_bytes: stat.model_size_stats.peak_model_bytes, + }, + timing_stats: { + average_bucket_processing_time_ms: stat.timing_stats.average_bucket_processing_time_ms, + bucket_count: stat.timing_stats.bucket_count, + exponential_average_bucket_processing_time_ms: + stat.timing_stats.exponential_average_bucket_processing_time_ms, + exponential_average_bucket_processing_time_per_hour_ms: + stat.timing_stats.exponential_average_bucket_processing_time_per_hour_ms, + maximum_bucket_processing_time_ms: stat.timing_stats.maximum_bucket_processing_time_ms, + minimum_bucket_processing_time_ms: stat.timing_stats.minimum_bucket_processing_time_ms, + total_bucket_processing_time_ms: stat.timing_stats.total_bucket_processing_time_ms, + }, + datafeed: { + datafeed_id: datafeed?.datafeed_id, + state: datafeed?.state, + timing_stats: { + bucket_count: datafeed?.timing_stats.bucket_count, + exponential_average_search_time_per_hour_ms: + datafeed?.timing_stats.exponential_average_search_time_per_hour_ms, + search_count: datafeed?.timing_stats.search_count, + total_search_time_ms: datafeed?.timing_stats.total_search_time_ms, + }, + }, + } as MlJobMetric; + }); + + return { + ml_job_usage: jobsUsage, + ml_job_metrics: jobMetrics, + }; + } catch (e) { + // ignore failure, usage will be zeroed + } + } + + return { + ml_job_usage: initialMlJobsUsage, + ml_job_metrics: [], + }; +}; diff --git a/x-pack/plugins/security_solution/server/usage/detections/dectections_metrics_helpers.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.test.ts similarity index 98% rename from x-pack/plugins/security_solution/server/usage/detections/dectections_metrics_helpers.test.ts rename to x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.test.ts index 55bd372e9dd799..d1846caf4ec223 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/dectections_metrics_helpers.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.test.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { initialDetectionRulesUsage, updateDetectionRuleUsage } from './detections_metrics_helpers'; -import { DetectionRuleMetric, DetectionRulesTypeUsage } from './index'; +import { initialDetectionRulesUsage, updateDetectionRuleUsage } from './detection_rule_helpers'; +import { DetectionRuleMetric, DetectionRulesTypeUsage } from './types'; import { v4 as uuid } from 'uuid'; const createStubRule = ( diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_metrics_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts similarity index 68% rename from x-pack/plugins/security_solution/server/usage/detections/detections_metrics_helpers.ts rename to x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts index a84ea108c5f121..ebcda694411355 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_metrics_helpers.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detection_rule_helpers.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { - ElasticsearchClient, - KibanaRequest, - SavedObjectsClientContract, -} from '../../../../../../src/core/server'; +import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server'; +import { SIGNALS_ID } from '../../../common/constants'; +import { isElasticRule } from './index'; import { AlertsAggregationResponse, CasesSavedObject, DetectionRulesTypeUsage, DetectionRuleMetric, DetectionRuleAdoption, - MlJobMetric, -} from './index'; -import { SIGNALS_ID } from '../../../common/constants'; -import { DatafeedStats, Job, MlPluginSetup } from '../../../../ml/server'; -import { isElasticRule, RuleSearchParams, RuleSearchResult } from './detection_telemetry_helpers'; + RuleSearchParams, + RuleSearchResult, +} from './types'; /** * Default detection rule usage count, split by type + elastic/custom @@ -288,91 +284,3 @@ export const getDetectionRuleMetrics = async ( detection_rule_usage: rulesUsage, }; }; - -export const getMlJobMetrics = async ( - ml: MlPluginSetup | undefined, - savedObjectClient: SavedObjectsClientContract -): Promise => { - if (ml) { - try { - const fakeRequest = { headers: {} } as KibanaRequest; - const jobsType = 'security'; - const securityJobStats = await ml - .anomalyDetectorsProvider(fakeRequest, savedObjectClient) - .jobStats(jobsType); - - const jobDetails = await ml - .anomalyDetectorsProvider(fakeRequest, savedObjectClient) - .jobs(jobsType); - - const jobDetailsCache = new Map(); - jobDetails.jobs.forEach((detail) => jobDetailsCache.set(detail.job_id, detail)); - - const datafeedStats = await ml - .anomalyDetectorsProvider(fakeRequest, savedObjectClient) - .datafeedStats(); - - const datafeedStatsCache = new Map(); - datafeedStats.datafeeds.forEach((datafeedStat) => - datafeedStatsCache.set(`${datafeedStat.datafeed_id}`, datafeedStat) - ); - - return securityJobStats.jobs.map((stat) => { - const jobId = stat.job_id; - const jobDetail = jobDetailsCache.get(stat.job_id); - const datafeed = datafeedStatsCache.get(`datafeed-${jobId}`); - - return { - job_id: jobId, - open_time: stat.open_time, - create_time: jobDetail?.create_time, - finished_time: jobDetail?.finished_time, - state: stat.state, - data_counts: { - bucket_count: stat.data_counts.bucket_count, - empty_bucket_count: stat.data_counts.empty_bucket_count, - input_bytes: stat.data_counts.input_bytes, - input_record_count: stat.data_counts.input_record_count, - last_data_time: stat.data_counts.last_data_time, - processed_record_count: stat.data_counts.processed_record_count, - }, - model_size_stats: { - bucket_allocation_failures_count: - stat.model_size_stats.bucket_allocation_failures_count, - memory_status: stat.model_size_stats.memory_status, - model_bytes: stat.model_size_stats.model_bytes, - model_bytes_exceeded: stat.model_size_stats.model_bytes_exceeded, - model_bytes_memory_limit: stat.model_size_stats.model_bytes_memory_limit, - peak_model_bytes: stat.model_size_stats.peak_model_bytes, - }, - timing_stats: { - average_bucket_processing_time_ms: stat.timing_stats.average_bucket_processing_time_ms, - bucket_count: stat.timing_stats.bucket_count, - exponential_average_bucket_processing_time_ms: - stat.timing_stats.exponential_average_bucket_processing_time_ms, - exponential_average_bucket_processing_time_per_hour_ms: - stat.timing_stats.exponential_average_bucket_processing_time_per_hour_ms, - maximum_bucket_processing_time_ms: stat.timing_stats.maximum_bucket_processing_time_ms, - minimum_bucket_processing_time_ms: stat.timing_stats.minimum_bucket_processing_time_ms, - total_bucket_processing_time_ms: stat.timing_stats.total_bucket_processing_time_ms, - }, - datafeed: { - datafeed_id: datafeed?.datafeed_id, - state: datafeed?.state, - timing_stats: { - bucket_count: datafeed?.timing_stats.bucket_count, - exponential_average_search_time_per_hour_ms: - datafeed?.timing_stats.exponential_average_search_time_per_hour_ms, - search_count: datafeed?.timing_stats.search_count, - total_search_time_ms: datafeed?.timing_stats.total_search_time_ms, - }, - }, - } as MlJobMetric; - }); - } catch (e) { - // ignore failure, usage will be zeroed - } - } - - return []; -}; diff --git a/x-pack/plugins/security_solution/server/usage/detections/detection_telemetry_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detection_telemetry_helpers.ts deleted file mode 100644 index bc1e734e4cc3af..00000000000000 --- a/x-pack/plugins/security_solution/server/usage/detections/detection_telemetry_helpers.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 { INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; - -export const isElasticRule = (tags: string[] = []) => - tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); - -interface RuleSearchBody { - query: { - bool: { - filter: { - term: { [key: string]: string }; - }; - }; - }; -} - -export interface RuleSearchParams { - body: RuleSearchBody; - filterPath: string[]; - ignoreUnavailable: boolean; - index: string; - size: number; -} - -export interface RuleSearchResult { - alert: { - name: string; - enabled: boolean; - tags: string[]; - createdAt: string; - updatedAt: string; - params: DetectionRuleParms; - }; -} - -interface DetectionRuleParms { - ruleId: string; - version: string; - type: string; -} diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts index f90841ff4e596c..c4ab55127f5d3e 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.mocks.ts @@ -5,8 +5,6 @@ * 2.0. */ -import { INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; - export const getMockJobSummaryResponse = () => [ { id: 'linux_anomalous_network_activity_ecs', @@ -162,20 +160,6 @@ export const getMockListModulesResponse = () => [ }, ]; -export const getMockRulesResponse = () => ({ - hits: { - hits: [ - { _source: { alert: { enabled: true, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } }, - { _source: { alert: { enabled: true, tags: [`${INTERNAL_IMMUTABLE_KEY}:false`] } } }, - { _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } }, - { _source: { alert: { enabled: true, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } }, - { _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:false`] } } }, - { _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } }, - { _source: { alert: { enabled: false, tags: [`${INTERNAL_IMMUTABLE_KEY}:true`] } } }, - ], - }, -}); - export const getMockMlJobDetailsResponse = () => ({ count: 20, jobs: [ diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts index a020f49464007f..7365c210172fe4 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/detections.test.ts @@ -11,10 +11,11 @@ import { savedObjectsClientMock, } from '../../../../../../src/core/server/mocks'; import { mlServicesMock } from '../../lib/machine_learning/mocks'; +import { fetchDetectionsMetrics } from './index'; +import { initialMlJobsUsage } from './detection_ml_helpers'; import { getMockJobSummaryResponse, getMockListModulesResponse, - getMockRulesResponse, getMockMlJobDetailsResponse, getMockMlJobStatsResponse, getMockMlDatafeedStatsResponse, @@ -22,7 +23,6 @@ import { getMockRuleAlertsResponse, getMockAlertCasesResponse, } from './detections.mocks'; -import { fetchDetectionsUsage, fetchDetectionsMetrics } from './index'; const savedObjectsClient = savedObjectsClientMock.create(); @@ -30,89 +30,6 @@ describe('Detections Usage and Metrics', () => { let esClientMock: jest.Mocked; let mlMock: ReturnType; - describe('fetchDetectionsUsage()', () => { - beforeEach(() => { - esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; - mlMock = mlServicesMock.createSetupContract(); - }); - - it('returns zeroed counts if both calls are empty', async () => { - const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient); - - expect(result).toEqual({ - detection_rules: { - custom: { - enabled: 0, - disabled: 0, - }, - elastic: { - enabled: 0, - disabled: 0, - }, - }, - ml_jobs: { - custom: { - enabled: 0, - disabled: 0, - }, - elastic: { - enabled: 0, - disabled: 0, - }, - }, - }); - }); - - it('tallies rules data given rules results', async () => { - (esClientMock.search as jest.Mock).mockResolvedValue({ body: getMockRulesResponse() }); - - const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient); - - expect(result).toEqual( - expect.objectContaining({ - detection_rules: { - custom: { - enabled: 1, - disabled: 1, - }, - elastic: { - enabled: 2, - disabled: 3, - }, - }, - }) - ); - }); - - it('tallies jobs data given jobs results', async () => { - const mockJobSummary = jest.fn().mockResolvedValue(getMockJobSummaryResponse()); - const mockListModules = jest.fn().mockResolvedValue(getMockListModulesResponse()); - mlMock.modulesProvider.mockReturnValue(({ - listModules: mockListModules, - } as unknown) as ReturnType); - mlMock.jobServiceProvider.mockReturnValue({ - jobsSummary: mockJobSummary, - }); - - const result = await fetchDetectionsUsage('', esClientMock, mlMock, savedObjectsClient); - - expect(result).toEqual( - expect.objectContaining({ - ml_jobs: { - custom: { - enabled: 1, - disabled: 1, - }, - elastic: { - enabled: 1, - disabled: 1, - }, - }, - }) - ); - }); - }); - describe('getDetectionRuleMetrics()', () => { beforeEach(() => { esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -171,7 +88,7 @@ describe('Detections Usage and Metrics', () => { }, }, }, - ml_jobs: [], + ml_jobs: { ml_job_metrics: [], ml_job_usage: initialMlJobsUsage }, }) ); }); @@ -246,7 +163,7 @@ describe('Detections Usage and Metrics', () => { }, }, }, - ml_jobs: [], + ml_jobs: { ml_job_metrics: [], ml_job_usage: initialMlJobsUsage }, }) ); }); @@ -308,7 +225,7 @@ describe('Detections Usage and Metrics', () => { }, }, }, - ml_jobs: [], + ml_jobs: { ml_job_metrics: [], ml_job_usage: initialMlJobsUsage }, }) ); }); @@ -383,7 +300,7 @@ describe('Detections Usage and Metrics', () => { }, }, }, - ml_jobs: [], + ml_jobs: { ml_job_metrics: [], ml_job_usage: initialMlJobsUsage }, }) ); }); @@ -404,12 +321,20 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual( expect.objectContaining({ - ml_jobs: [], + ml_jobs: { ml_job_metrics: [], ml_job_usage: initialMlJobsUsage }, }) ); }); it('returns an ml job telemetry object from anomaly detectors provider', async () => { + const mockJobSummary = jest.fn().mockResolvedValue(getMockJobSummaryResponse()); + const mockListModules = jest.fn().mockResolvedValue(getMockListModulesResponse()); + mlMock.modulesProvider.mockReturnValue(({ + listModules: mockListModules, + } as unknown) as ReturnType); + mlMock.jobServiceProvider.mockReturnValue({ + jobsSummary: mockJobSummary, + }); const mockJobsResponse = jest.fn().mockResolvedValue(getMockMlJobDetailsResponse()); const mockJobStatsResponse = jest.fn().mockResolvedValue(getMockMlJobStatsResponse()); const mockDatafeedStatsResponse = jest @@ -426,49 +351,61 @@ describe('Detections Usage and Metrics', () => { expect(result).toEqual( expect.objectContaining({ - ml_jobs: [ - { - job_id: 'high_distinct_count_error_message', - create_time: 1603838214983, - finished_time: 1611739871669, - state: 'closed', - data_counts: { - bucket_count: 8612, - empty_bucket_count: 8590, - input_bytes: 45957, - input_record_count: 162, - last_data_time: 1610470367123, - processed_record_count: 162, - }, - model_size_stats: { - bucket_allocation_failures_count: 0, - memory_status: 'ok', - model_bytes: 72574, - model_bytes_exceeded: 0, - model_bytes_memory_limit: 16777216, - peak_model_bytes: 78682, - }, - timing_stats: { - average_bucket_processing_time_ms: 0.4900837644740133, - bucket_count: 16236, - exponential_average_bucket_processing_time_ms: 0.23614068552903306, - exponential_average_bucket_processing_time_per_hour_ms: 1.5551298175461634, - maximum_bucket_processing_time_ms: 392, - minimum_bucket_processing_time_ms: 0, - total_bucket_processing_time_ms: 7957.00000000008, - }, - datafeed: { - datafeed_id: 'datafeed-high_distinct_count_error_message', - state: 'stopped', - timing_stats: { + ml_jobs: { + ml_job_usage: { + custom: { + disabled: 1, + enabled: 1, + }, + elastic: { + disabled: 1, + enabled: 1, + }, + }, + ml_job_metrics: [ + { + job_id: 'high_distinct_count_error_message', + create_time: 1603838214983, + finished_time: 1611739871669, + state: 'closed', + data_counts: { bucket_count: 8612, - exponential_average_search_time_per_hour_ms: 86145.39799630083, - search_count: 7202, - total_search_time_ms: 3107147, + empty_bucket_count: 8590, + input_bytes: 45957, + input_record_count: 162, + last_data_time: 1610470367123, + processed_record_count: 162, + }, + model_size_stats: { + bucket_allocation_failures_count: 0, + memory_status: 'ok', + model_bytes: 72574, + model_bytes_exceeded: 0, + model_bytes_memory_limit: 16777216, + peak_model_bytes: 78682, + }, + timing_stats: { + average_bucket_processing_time_ms: 0.4900837644740133, + bucket_count: 16236, + exponential_average_bucket_processing_time_ms: 0.23614068552903306, + exponential_average_bucket_processing_time_per_hour_ms: 1.5551298175461634, + maximum_bucket_processing_time_ms: 392, + minimum_bucket_processing_time_ms: 0, + total_bucket_processing_time_ms: 7957.00000000008, + }, + datafeed: { + datafeed_id: 'datafeed-high_distinct_count_error_message', + state: 'stopped', + timing_stats: { + bucket_count: 8612, + exponential_average_search_time_per_hour_ms: 86145.39799630083, + search_count: 7202, + total_search_time_ms: 3107147, + }, }, }, - }, - ], + ], + }, }) ); }); diff --git a/x-pack/plugins/security_solution/server/usage/detections/detections_usage_helpers.ts b/x-pack/plugins/security_solution/server/usage/detections/detections_usage_helpers.ts deleted file mode 100644 index 3c666d4d217809..00000000000000 --- a/x-pack/plugins/security_solution/server/usage/detections/detections_usage_helpers.ts +++ /dev/null @@ -1,191 +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 { - ElasticsearchClient, - KibanaRequest, - SavedObjectsClientContract, -} from '../../../../../../src/core/server'; -import { SIGNALS_ID } from '../../../common/constants'; -import { isJobStarted } from '../../../common/machine_learning/helpers'; -import { isSecurityJob } from '../../../common/machine_learning/is_security_job'; -import { MlPluginSetup } from '../../../../ml/server'; -import { DetectionRulesUsage, MlJobsUsage } from './index'; -import { isElasticRule, RuleSearchParams, RuleSearchResult } from './detection_telemetry_helpers'; - -interface DetectionsMetric { - isElastic: boolean; - isEnabled: boolean; -} - -/** - * Default detection rule usage count - */ -export const initialRulesUsage: DetectionRulesUsage = { - custom: { - enabled: 0, - disabled: 0, - }, - elastic: { - enabled: 0, - disabled: 0, - }, -}; - -/** - * Default ml job usage count - */ -export const initialMlJobsUsage: MlJobsUsage = { - custom: { - enabled: 0, - disabled: 0, - }, - elastic: { - enabled: 0, - disabled: 0, - }, -}; - -const updateRulesUsage = ( - ruleMetric: DetectionsMetric, - usage: DetectionRulesUsage -): DetectionRulesUsage => { - const { isEnabled, isElastic } = ruleMetric; - if (isEnabled && isElastic) { - return { - ...usage, - elastic: { - ...usage.elastic, - enabled: usage.elastic.enabled + 1, - }, - }; - } else if (!isEnabled && isElastic) { - return { - ...usage, - elastic: { - ...usage.elastic, - disabled: usage.elastic.disabled + 1, - }, - }; - } else if (isEnabled && !isElastic) { - return { - ...usage, - custom: { - ...usage.custom, - enabled: usage.custom.enabled + 1, - }, - }; - } else if (!isEnabled && !isElastic) { - return { - ...usage, - custom: { - ...usage.custom, - disabled: usage.custom.disabled + 1, - }, - }; - } else { - return usage; - } -}; - -const updateMlJobsUsage = (jobMetric: DetectionsMetric, usage: MlJobsUsage): MlJobsUsage => { - const { isEnabled, isElastic } = jobMetric; - if (isEnabled && isElastic) { - return { - ...usage, - elastic: { - ...usage.elastic, - enabled: usage.elastic.enabled + 1, - }, - }; - } else if (!isEnabled && isElastic) { - return { - ...usage, - elastic: { - ...usage.elastic, - disabled: usage.elastic.disabled + 1, - }, - }; - } else if (isEnabled && !isElastic) { - return { - ...usage, - custom: { - ...usage.custom, - enabled: usage.custom.enabled + 1, - }, - }; - } else if (!isEnabled && !isElastic) { - return { - ...usage, - custom: { - ...usage.custom, - disabled: usage.custom.disabled + 1, - }, - }; - } else { - return usage; - } -}; - -export const getRulesUsage = async ( - index: string, - esClient: ElasticsearchClient -): Promise => { - let rulesUsage: DetectionRulesUsage = initialRulesUsage; - const ruleSearchOptions: RuleSearchParams = { - body: { query: { bool: { filter: { term: { 'alert.alertTypeId': SIGNALS_ID } } } } }, - filterPath: ['hits.hits._source.alert.enabled', 'hits.hits._source.alert.tags'], - ignoreUnavailable: true, - index, - size: 10000, // elasticsearch index.max_result_window default value - }; - - try { - const { body: ruleResults } = await esClient.search(ruleSearchOptions); - - if (ruleResults.hits?.hits?.length > 0) { - rulesUsage = ruleResults.hits.hits.reduce((usage, hit) => { - const isElastic = isElasticRule(hit._source?.alert.tags); - const isEnabled = Boolean(hit._source?.alert.enabled); - - return updateRulesUsage({ isElastic, isEnabled }, usage); - }, initialRulesUsage); - } - } catch (e) { - // ignore failure, usage will be zeroed - } - - return rulesUsage; -}; - -export const getMlJobsUsage = async ( - ml: MlPluginSetup | undefined, - savedObjectClient: SavedObjectsClientContract -): Promise => { - let jobsUsage: MlJobsUsage = initialMlJobsUsage; - - if (ml) { - try { - const fakeRequest = { headers: {} } as KibanaRequest; - - const modules = await ml.modulesProvider(fakeRequest, savedObjectClient).listModules(); - const moduleJobs = modules.flatMap((module) => module.jobs); - const jobs = await ml.jobServiceProvider(fakeRequest, savedObjectClient).jobsSummary(); - - jobsUsage = jobs.filter(isSecurityJob).reduce((usage, job) => { - const isElastic = moduleJobs.some((moduleJob) => moduleJob.id === job.id); - const isEnabled = isJobStarted(job.jobState, job.datafeedState); - - return updateMlJobsUsage({ isElastic, isEnabled }, usage); - }, initialMlJobsUsage); - } catch (e) { - // ignore failure, usage will be zeroed - } - } - - return jobsUsage; -}; diff --git a/x-pack/plugins/security_solution/server/usage/detections/index.ts b/x-pack/plugins/security_solution/server/usage/detections/index.ts index ea3df7b1f2230b..823e29fd0dd30d 100644 --- a/x-pack/plugins/security_solution/server/usage/detections/index.ts +++ b/x-pack/plugins/security_solution/server/usage/detections/index.ts @@ -6,157 +6,15 @@ */ import { ElasticsearchClient, SavedObjectsClientContract } from '../../../../../../src/core/server'; -import { - getMlJobsUsage, - getRulesUsage, - initialRulesUsage, - initialMlJobsUsage, -} from './detections_usage_helpers'; -import { - getMlJobMetrics, - getDetectionRuleMetrics, - initialDetectionRulesUsage, -} from './detections_metrics_helpers'; import { MlPluginSetup } from '../../../../ml/server'; +import { getDetectionRuleMetrics, initialDetectionRulesUsage } from './detection_rule_helpers'; +import { getMlJobMetrics, initialMlJobsUsage } from './detection_ml_helpers'; +import { DetectionMetrics } from './types'; -interface FeatureUsage { - enabled: number; - disabled: number; -} +import { INTERNAL_IMMUTABLE_KEY } from '../../../common/constants'; -interface FeatureTypeUsage { - enabled: number; - disabled: number; - alerts: number; - cases: number; -} - -export interface DetectionRulesTypeUsage { - query: FeatureTypeUsage; - threshold: FeatureTypeUsage; - eql: FeatureTypeUsage; - machine_learning: FeatureTypeUsage; - threat_match: FeatureTypeUsage; - elastic_total: FeatureTypeUsage; - custom_total: FeatureTypeUsage; -} - -export interface DetectionRulesUsage { - custom: FeatureUsage; - elastic: FeatureUsage; -} - -export interface MlJobsUsage { - custom: FeatureUsage; - elastic: FeatureUsage; -} - -export interface DetectionsUsage { - detection_rules: DetectionRulesUsage; - ml_jobs: MlJobsUsage; -} - -export interface DetectionMetrics { - ml_jobs: MlJobMetric[]; - detection_rules: DetectionRuleAdoption; -} - -export interface MlJobDataCount { - bucket_count: number; - empty_bucket_count: number; - input_bytes: number; - input_record_count: number; - last_data_time: number; - processed_record_count: number; -} - -export interface MlJobModelSize { - bucket_allocation_failures_count: number; - memory_status: string; - model_bytes: number; - model_bytes_exceeded: number; - model_bytes_memory_limit: number; - peak_model_bytes: number; -} - -export interface MlTimingStats { - bucket_count: number; - exponential_average_bucket_processing_time_ms: number; - exponential_average_bucket_processing_time_per_hour_ms: number; - maximum_bucket_processing_time_ms: number; - minimum_bucket_processing_time_ms: number; - total_bucket_processing_time_ms: number; -} - -export interface MlJobMetric { - job_id: string; - open_time: string; - state: string; - data_counts: MlJobDataCount; - model_size_stats: MlJobModelSize; - timing_stats: MlTimingStats; -} - -export interface DetectionRuleMetric { - rule_name: string; - rule_id: string; - rule_type: string; - enabled: boolean; - elastic_rule: boolean; - created_on: string; - updated_on: string; - alert_count_daily: number; - cases_count_total: number; -} - -export interface DetectionRuleAdoption { - detection_rule_detail: DetectionRuleMetric[]; - detection_rule_usage: DetectionRulesTypeUsage; -} - -export interface AlertsAggregationResponse { - hits: { - total: { value: number }; - }; - aggregations: { - [aggName: string]: { - buckets: Array<{ key: string; doc_count: number }>; - }; - }; -} - -export interface CasesSavedObject { - associationType: string; - type: string; - alertId: string; - index: string; - rule: { - id: string; - name: string; - }; -} - -export const defaultDetectionsUsage = { - detection_rules: initialRulesUsage, - ml_jobs: initialMlJobsUsage, -}; - -export const fetchDetectionsUsage = async ( - kibanaIndex: string, - esClient: ElasticsearchClient, - ml: MlPluginSetup | undefined, - savedObjectClient: SavedObjectsClientContract -): Promise => { - const [rulesUsage, mlJobsUsage] = await Promise.allSettled([ - getRulesUsage(kibanaIndex, esClient), - getMlJobsUsage(ml, savedObjectClient), - ]); - - return { - detection_rules: rulesUsage.status === 'fulfilled' ? rulesUsage.value : initialRulesUsage, - ml_jobs: mlJobsUsage.status === 'fulfilled' ? mlJobsUsage.value : initialMlJobsUsage, - }; -}; +export const isElasticRule = (tags: string[] = []) => + tags.includes(`${INTERNAL_IMMUTABLE_KEY}:true`); export const fetchDetectionsMetrics = async ( kibanaIndex: string, @@ -171,7 +29,10 @@ export const fetchDetectionsMetrics = async ( ]); return { - ml_jobs: mlJobMetrics.status === 'fulfilled' ? mlJobMetrics.value : [], + ml_jobs: + mlJobMetrics.status === 'fulfilled' + ? mlJobMetrics.value + : { ml_job_metrics: [], ml_job_usage: initialMlJobsUsage }, detection_rules: detectionRuleMetrics.status === 'fulfilled' ? detectionRuleMetrics.value diff --git a/x-pack/plugins/security_solution/server/usage/detections/types.ts b/x-pack/plugins/security_solution/server/usage/detections/types.ts new file mode 100644 index 00000000000000..0e3ba97ca0f7c2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/usage/detections/types.ts @@ -0,0 +1,162 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +interface RuleSearchBody { + query: { + bool: { + filter: { + term: { [key: string]: string }; + }; + }; + }; +} + +export interface RuleSearchParams { + body: RuleSearchBody; + filterPath: string[]; + ignoreUnavailable: boolean; + index: string; + size: number; +} + +export interface RuleSearchResult { + alert: { + name: string; + enabled: boolean; + tags: string[]; + createdAt: string; + updatedAt: string; + params: DetectionRuleParms; + }; +} + +export interface DetectionsMetric { + isElastic: boolean; + isEnabled: boolean; +} + +interface DetectionRuleParms { + ruleId: string; + version: string; + type: string; +} + +interface FeatureUsage { + enabled: number; + disabled: number; +} + +interface FeatureTypeUsage { + enabled: number; + disabled: number; + alerts: number; + cases: number; +} + +export interface DetectionRulesTypeUsage { + query: FeatureTypeUsage; + threshold: FeatureTypeUsage; + eql: FeatureTypeUsage; + machine_learning: FeatureTypeUsage; + threat_match: FeatureTypeUsage; + elastic_total: FeatureTypeUsage; + custom_total: FeatureTypeUsage; +} + +export interface MlJobsUsage { + custom: FeatureUsage; + elastic: FeatureUsage; +} + +export interface DetectionsUsage { + ml_jobs: MlJobsUsage; +} + +export interface DetectionMetrics { + ml_jobs: MlJobUsage; + detection_rules: DetectionRuleAdoption; +} + +export interface MlJobDataCount { + bucket_count: number; + empty_bucket_count: number; + input_bytes: number; + input_record_count: number; + last_data_time: number; + processed_record_count: number; +} + +export interface MlJobModelSize { + bucket_allocation_failures_count: number; + memory_status: string; + model_bytes: number; + model_bytes_exceeded: number; + model_bytes_memory_limit: number; + peak_model_bytes: number; +} + +export interface MlTimingStats { + bucket_count: number; + exponential_average_bucket_processing_time_ms: number; + exponential_average_bucket_processing_time_per_hour_ms: number; + maximum_bucket_processing_time_ms: number; + minimum_bucket_processing_time_ms: number; + total_bucket_processing_time_ms: number; +} + +export interface MlJobMetric { + job_id: string; + open_time: string; + state: string; + data_counts: MlJobDataCount; + model_size_stats: MlJobModelSize; + timing_stats: MlTimingStats; +} + +export interface DetectionRuleMetric { + rule_name: string; + rule_id: string; + rule_type: string; + enabled: boolean; + elastic_rule: boolean; + created_on: string; + updated_on: string; + alert_count_daily: number; + cases_count_total: number; +} + +export interface AlertsAggregationResponse { + hits: { + total: { value: number }; + }; + aggregations: { + [aggName: string]: { + buckets: Array<{ key: string; doc_count: number }>; + }; + }; +} + +export interface CasesSavedObject { + associationType: string; + type: string; + alertId: string; + index: string; + rule: { + id: string; + name: string; + }; +} + +export interface MlJobUsage { + ml_job_usage: MlJobsUsage; + ml_job_metrics: MlJobMetric[]; +} + +export interface DetectionRuleAdoption { + detection_rule_detail: DetectionRuleMetric[]; + detection_rule_usage: DetectionRulesTypeUsage; +} diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx index 392219d480e671..6f743b3a308e25 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx @@ -71,9 +71,6 @@ export class NavControlPopover extends Component { public render() { const button = this.getActiveSpaceButton(); - if (!button) { - return null; - } let element: React.ReactNode; if (!this.state.loading && this.state.spaces.length < 2) { @@ -102,7 +99,7 @@ export class NavControlPopover extends Component { return ( = {}) => { const deprecations = spacesConfigDeprecationProvider(configDeprecationFactory); const deprecationMessages: string[] = []; - const migrated = applyDeprecations( + const { config: migrated } = applyDeprecations( settings, deprecations.map((deprecation) => ({ deprecation, diff --git a/x-pack/plugins/stack_alerts/server/feature.test.ts b/x-pack/plugins/stack_alerts/server/feature.test.ts index 62807f1c10a1ca..3dde7fd09f3476 100644 --- a/x-pack/plugins/stack_alerts/server/feature.test.ts +++ b/x-pack/plugins/stack_alerts/server/feature.test.ts @@ -20,9 +20,11 @@ describe('Stack Alerts Feature Privileges', () => { const featuresSetup = featuresPluginMock.createSetup(); await plugin.setup(coreSetup, { alerting: alertingSetup, features: featuresSetup }); - const typesInFeaturePrivilege = BUILT_IN_ALERTS_FEATURE.alerting; - const typesInFeaturePrivilegeAll = BUILT_IN_ALERTS_FEATURE.privileges.all.alerting.all; - const typesInFeaturePrivilegeRead = BUILT_IN_ALERTS_FEATURE.privileges.read.alerting.read; + const typesInFeaturePrivilege = BUILT_IN_ALERTS_FEATURE.alerting ?? []; + const typesInFeaturePrivilegeAll = + BUILT_IN_ALERTS_FEATURE.privileges?.all?.alerting?.rule?.all ?? []; + const typesInFeaturePrivilegeRead = + BUILT_IN_ALERTS_FEATURE.privileges?.read?.alerting?.rule?.read ?? []; expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilege.length); expect(alertingSetup.registerType.mock.calls.length).toEqual(typesInFeaturePrivilegeAll.length); expect(alertingSetup.registerType.mock.calls.length).toEqual( diff --git a/x-pack/plugins/stack_alerts/server/feature.ts b/x-pack/plugins/stack_alerts/server/feature.ts index e168ec21438c0c..70e68c2b7ced30 100644 --- a/x-pack/plugins/stack_alerts/server/feature.ts +++ b/x-pack/plugins/stack_alerts/server/feature.ts @@ -6,13 +6,14 @@ */ import { i18n } from '@kbn/i18n'; +import { KibanaFeatureConfig } from '../../../plugins/features/common'; import { ID as IndexThreshold } from './alert_types/index_threshold/alert_type'; import { GEO_CONTAINMENT_ID as GeoContainment } from './alert_types/geo_containment/alert_type'; import { ES_QUERY_ID as ElasticsearchQuery } from './alert_types/es_query/alert_type'; import { STACK_ALERTS_FEATURE_ID } from '../common'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; -export const BUILT_IN_ALERTS_FEATURE = { +export const BUILT_IN_ALERTS_FEATURE: KibanaFeatureConfig = { id: STACK_ALERTS_FEATURE_ID, name: i18n.translate('xpack.stackAlerts.featureRegistry.actionsFeatureName', { defaultMessage: 'Stack Rules', @@ -31,8 +32,12 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [IndexThreshold, GeoContainment, ElasticsearchQuery], - read: [], + rule: { + all: [IndexThreshold, GeoContainment, ElasticsearchQuery], + }, + alert: { + all: [IndexThreshold, GeoContainment, ElasticsearchQuery], + }, }, savedObject: { all: [], @@ -48,8 +53,12 @@ export const BUILT_IN_ALERTS_FEATURE = { insightsAndAlerting: ['triggersActions'], }, alerting: { - all: [], - read: [IndexThreshold, GeoContainment, ElasticsearchQuery], + rule: { + read: [IndexThreshold, GeoContainment, ElasticsearchQuery], + }, + alert: { + read: [IndexThreshold, GeoContainment, ElasticsearchQuery], + }, }, savedObject: { all: [], diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 3fce5f7bdfdf48..8eb98c39a2ccd2 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -16,7 +16,7 @@ const applyTaskManagerDeprecations = (settings: Record = {}) => const _config = { [CONFIG_PATH]: settings, }; - const migrated = applyDeprecations( + const { config: migrated } = applyDeprecations( _config, deprecations.map((deprecation) => ({ deprecation, diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index b85fd8bf8989e3..12e83008b2e5a5 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -4664,82 +4664,6 @@ }, "security_solution": { "properties": { - "detections": { - "properties": { - "detection_rules": { - "properties": { - "custom": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "The number of custom detection rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "The number of custom detection rules disabled" - } - } - } - }, - "elastic": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "The number of elastic prebuilt detection rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "The number of elastic prebuilt detection rules disabled" - } - } - } - } - } - }, - "ml_jobs": { - "properties": { - "custom": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "The number of custom ML jobs rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "The number of custom ML jobs rules disabled" - } - } - } - }, - "elastic": { - "properties": { - "enabled": { - "type": "long", - "_meta": { - "description": "The number of elastic provided ML jobs rules enabled" - } - }, - "disabled": { - "type": "long", - "_meta": { - "description": "The number of elastic provided ML jobs rules disabled" - } - } - } - } - } - } - } - }, "detectionMetrics": { "properties": { "detection_rules": { @@ -5014,197 +4938,237 @@ } }, "ml_jobs": { - "type": "array", - "items": { - "properties": { - "job_id": { - "type": "keyword", - "_meta": { - "description": "Identifier for the anomaly detection job" - } - }, - "open_time": { - "type": "keyword", - "_meta": { - "description": "For open jobs only, the elapsed time for which the job has been open" - } - }, - "create_time": { - "type": "keyword", - "_meta": { - "description": "The time the job was created" - } - }, - "finished_time": { - "type": "keyword", - "_meta": { - "description": "If the job closed or failed, this is the time the job finished" - } - }, - "state": { - "type": "keyword", - "_meta": { - "description": "The status of the anomaly detection job" - } - }, - "data_counts": { - "properties": { - "bucket_count": { - "type": "long", - "_meta": { - "description": "The number of buckets processed" - } - }, - "empty_bucket_count": { - "type": "long", - "_meta": { - "description": "The number of buckets which did not contain any data" - } - }, - "input_bytes": { - "type": "long", - "_meta": { - "description": "The number of bytes of input data posted to the anomaly detection job" - } - }, - "input_record_count": { - "type": "long", - "_meta": { - "description": "The number of input documents posted to the anomaly detection job" - } - }, - "last_data_time": { - "type": "long", - "_meta": { - "description": "The timestamp at which data was last analyzed, according to server time" - } - }, - "processed_record_count": { - "type": "long", - "_meta": { - "description": "The number of input documents that have been processed by the anomaly detection job" + "properties": { + "ml_job_usage": { + "properties": { + "custom": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "The number of custom ML jobs rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "The number of custom ML jobs rules disabled" + } } } - } - }, - "model_size_stats": { - "properties": { - "bucket_allocation_failures_count": { - "type": "long", - "_meta": { - "description": "The number of buckets for which new entities in incoming data were not processed due to insufficient model memory" - } - }, - "model_bytes": { - "type": "long", - "_meta": { - "description": "The number of bytes of memory used by the models" - } - }, - "model_bytes_exceeded": { - "type": "long", - "_meta": { - "description": "The number of bytes over the high limit for memory usage at the last allocation failure" - } - }, - "model_bytes_memory_limit": { - "type": "long", - "_meta": { - "description": "The upper limit for model memory usage, checked on increasing values" - } - }, - "peak_model_bytes": { - "type": "long", - "_meta": { - "description": "The peak number of bytes of memory ever used by the models" + }, + "elastic": { + "properties": { + "enabled": { + "type": "long", + "_meta": { + "description": "The number of elastic provided ML jobs rules enabled" + } + }, + "disabled": { + "type": "long", + "_meta": { + "description": "The number of elastic provided ML jobs rules disabled" + } } } } - }, - "timing_stats": { + } + }, + "ml_job_metrics": { + "type": "array", + "items": { "properties": { - "bucket_count": { - "type": "long", - "_meta": { - "description": "The number of buckets processed" - } - }, - "exponential_average_bucket_processing_time_ms": { - "type": "long", - "_meta": { - "description": "Exponential moving average of all bucket processing times, in milliseconds" - } - }, - "exponential_average_bucket_processing_time_per_hour_ms": { - "type": "long", + "job_id": { + "type": "keyword", "_meta": { - "description": "Exponentially-weighted moving average of bucket processing times calculated in a 1 hour time window, in milliseconds" + "description": "Identifier for the anomaly detection job" } }, - "maximum_bucket_processing_time_ms": { - "type": "long", + "open_time": { + "type": "keyword", "_meta": { - "description": "Maximum among all bucket processing times, in milliseconds" + "description": "For open jobs only, the elapsed time for which the job has been open" } }, - "minimum_bucket_processing_time_ms": { - "type": "long", + "create_time": { + "type": "keyword", "_meta": { - "description": "Minimum among all bucket processing times, in milliseconds" + "description": "The time the job was created" } }, - "total_bucket_processing_time_ms": { - "type": "long", - "_meta": { - "description": "Sum of all bucket processing times, in milliseconds" - } - } - } - }, - "datafeed": { - "properties": { - "datafeed_id": { + "finished_time": { "type": "keyword", "_meta": { - "description": "A numerical character string that uniquely identifies the datafeed" + "description": "If the job closed or failed, this is the time the job finished" } }, "state": { "type": "keyword", "_meta": { - "description": "The status of the datafeed" + "description": "The status of the anomaly detection job" } }, - "timing_stats": { + "data_counts": { + "properties": { + "bucket_count": { + "type": "long", + "_meta": { + "description": "The number of buckets processed" + } + }, + "empty_bucket_count": { + "type": "long", + "_meta": { + "description": "The number of buckets which did not contain any data" + } + }, + "input_bytes": { + "type": "long", + "_meta": { + "description": "The number of bytes of input data posted to the anomaly detection job" + } + }, + "input_record_count": { + "type": "long", + "_meta": { + "description": "The number of input documents posted to the anomaly detection job" + } + }, + "last_data_time": { + "type": "long", + "_meta": { + "description": "The timestamp at which data was last analyzed, according to server time" + } + }, + "processed_record_count": { + "type": "long", + "_meta": { + "description": "The number of input documents that have been processed by the anomaly detection job" + } + } + } + }, + "model_size_stats": { "properties": { - "average_search_time_per_bucket_ms": { + "bucket_allocation_failures_count": { + "type": "long", + "_meta": { + "description": "The number of buckets for which new entities in incoming data were not processed due to insufficient model memory" + } + }, + "model_bytes": { + "type": "long", + "_meta": { + "description": "The number of bytes of memory used by the models" + } + }, + "model_bytes_exceeded": { + "type": "long", + "_meta": { + "description": "The number of bytes over the high limit for memory usage at the last allocation failure" + } + }, + "model_bytes_memory_limit": { "type": "long", "_meta": { - "description": "The average search time per bucket, in milliseconds" + "description": "The upper limit for model memory usage, checked on increasing values" } }, + "peak_model_bytes": { + "type": "long", + "_meta": { + "description": "The peak number of bytes of memory ever used by the models" + } + } + } + }, + "timing_stats": { + "properties": { "bucket_count": { "type": "long", "_meta": { "description": "The number of buckets processed" } }, - "exponential_average_search_time_per_hour_ms": { + "exponential_average_bucket_processing_time_ms": { + "type": "long", + "_meta": { + "description": "Exponential moving average of all bucket processing times, in milliseconds" + } + }, + "exponential_average_bucket_processing_time_per_hour_ms": { "type": "long", "_meta": { - "description": "The exponential average search time per hour, in milliseconds" + "description": "Exponentially-weighted moving average of bucket processing times calculated in a 1 hour time window, in milliseconds" } }, - "search_count": { + "maximum_bucket_processing_time_ms": { "type": "long", "_meta": { - "description": "The number of searches run by the datafeed" + "description": "Maximum among all bucket processing times, in milliseconds" } }, - "total_search_time_ms": { + "minimum_bucket_processing_time_ms": { "type": "long", "_meta": { - "description": "The total time the datafeed spent searching, in milliseconds" + "description": "Minimum among all bucket processing times, in milliseconds" + } + }, + "total_bucket_processing_time_ms": { + "type": "long", + "_meta": { + "description": "Sum of all bucket processing times, in milliseconds" + } + } + } + }, + "datafeed": { + "properties": { + "datafeed_id": { + "type": "keyword", + "_meta": { + "description": "A numerical character string that uniquely identifies the datafeed" + } + }, + "state": { + "type": "keyword", + "_meta": { + "description": "The status of the datafeed" + } + }, + "timing_stats": { + "properties": { + "average_search_time_per_bucket_ms": { + "type": "long", + "_meta": { + "description": "The average search time per bucket, in milliseconds" + } + }, + "bucket_count": { + "type": "long", + "_meta": { + "description": "The number of buckets processed" + } + }, + "exponential_average_search_time_per_hour_ms": { + "type": "long", + "_meta": { + "description": "The exponential average search time per hour, in milliseconds" + } + }, + "search_count": { + "type": "long", + "_meta": { + "description": "The number of searches run by the datafeed" + } + }, + "total_search_time_ms": { + "type": "long", + "_meta": { + "description": "The total time the datafeed spent searching, in milliseconds" + } + } } } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a68efbf379ad45..d829c8eb22a98b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4215,7 +4215,7 @@ "visDefaultEditor.controls.ranges.fromLabel": "開始:", "visDefaultEditor.controls.ranges.greaterThanOrEqualPrepend": "≧", "visDefaultEditor.controls.ranges.greaterThanOrEqualTooltip": "よりも大きいまたは等しい", - "visDefaultEditor.controls.ranges.lessThanPrepend": "<", + "visDefaultEditor.controls.ranges.lessThanPrepend": "<", "visDefaultEditor.controls.ranges.lessThanTooltip": "より小さい", "visDefaultEditor.controls.ranges.removeRangeButtonAriaLabel": "{from}から{to}の範囲を削除", "visDefaultEditor.controls.ranges.toLabel": "終了:", @@ -6156,185 +6156,6 @@ "xpack.banners.settings.textColor.description": "バナーテキストの色を設定します。{subscriptionLink}", "xpack.banners.settings.textColor.title": "バナーテキスト色", "xpack.banners.settings.textContent.title": "バナーテキスト", - "xpack.beatsManagement.beat.actionSectionTypeLabel": "タイプ:{beatType}。", - "xpack.beatsManagement.beat.actionSectionVersionLabel": "バージョン:{beatVersion}", - "xpack.beatsManagement.beat.beatNameAndIdTitle": "ビート:{nameOrNoName} (ID:{id}) ", - "xpack.beatsManagement.beat.beatNotFoundErrorMessage": "ビートが見つかりません", - "xpack.beatsManagement.beat.beatNotFoundErrorTitle": "ビートが見つかりません", - "xpack.beatsManagement.beat.beatNotFoundMessage": "ビートが見つかりません", - "xpack.beatsManagement.beat.configTabLabel": "構成", - "xpack.beatsManagement.beat.configurationTagsTabLabel": "構成タグ", - "xpack.beatsManagement.beat.detailsConfigurationDescription": "1 つのタグに複数の構成を適用することができます。これらの構成は必要に応じて同じタイプを繰り返したり、タイプを組み合わせることができます。たとえば、3 つの Metricbeat 構成と同時に 1 つのインプットと Filebeat 構成を使用することができます。", - "xpack.beatsManagement.beat.detailsConfigurationTitle": "構成", - "xpack.beatsManagement.beat.lastConfigUpdateMessage": "前回の構成の更新:{lastUpdateTime}。", - "xpack.beatsManagement.beat.loadingTitle": "読み込み中", - "xpack.beatsManagement.beatConfigurations.descriptionColumnName": "説明", - "xpack.beatsManagement.beatConfigurations.moduleColumnName": "モジュール", - "xpack.beatsManagement.beatConfigurations.tagColumnName": "タグ", - "xpack.beatsManagement.beatConfigurations.typeColumnName": "型", - "xpack.beatsManagement.beats.beatDisenrolledNotificationDescription": "ID {firstBeatId} のビートの登録が解除されました。", - "xpack.beatsManagement.beats.beatDisenrolledNotificationTitle": "{firstBeatNameOrId} の登録解除", - "xpack.beatsManagement.beats.configurationTagsTabTitle": "構成タグ", - "xpack.beatsManagement.beats.disenrolledBeatsNotificationTitle": "{beatsLength} 個のビートの登録解除", - "xpack.beatsManagement.beats.enrollBeatsButtonLabel": "ビートを登録", - "xpack.beatsManagement.beats.enrolledBeatsTabTitle": "登録済みのビート", - "xpack.beatsManagement.beats.enrollNewBeatsTitle": "新規ビートの登録", - "xpack.beatsManagement.beats.installBeatsLearningButtonLabel": "ビートのインストール方法", - "xpack.beatsManagement.beatsListAssignmentOptions.setTagsButtonLabel": "タグを設定", - "xpack.beatsManagement.beatsListAssignmentOptions.unenrollBeatsWarninigMessage": "選択されたビートは今後集中管理を使用しません", - "xpack.beatsManagement.beatsListAssignmentOptions.unenrollBeatsWarninigTitle": "選択されたビートの登録を解除しますか?", - "xpack.beatsManagement.beatsListAssignmentOptions.unenrollButtonLabel": "選択項目の登録を解除", - "xpack.beatsManagement.beatsTable.beatNameTitle": "ビート名", - "xpack.beatsManagement.beatsTable.configErrorStatusLabel": "構成エラー", - "xpack.beatsManagement.beatsTable.configStatus.errorTooltip": "このビートにエラーがあります。このホストのログを確認してください。", - "xpack.beatsManagement.beatsTable.configStatus.noConnectionTooltip": "このビートは 10 分以上 Kibana に接続していません", - "xpack.beatsManagement.beatsTable.configStatus.notStartedLabel": "未開始", - "xpack.beatsManagement.beatsTable.configStatus.notStartedTooltip": "このビートはまだ開始していません。", - "xpack.beatsManagement.beatsTable.configStatus.offlineLabel": "オフライン", - "xpack.beatsManagement.beatsTable.configStatus.okLabel": "OK", - "xpack.beatsManagement.beatsTable.configStatus.okTooltip": "最新の構成にビートが適用されました", - "xpack.beatsManagement.beatsTable.configStatus.progressTooltip": "このビートは現在 CM から構成を再読み込み中です。", - "xpack.beatsManagement.beatsTable.configStatus.runningTooltip": "このビートは問題なく動作中です。", - "xpack.beatsManagement.beatsTable.configStatus.startingTooltip": "このビートは開始中です。", - "xpack.beatsManagement.beatsTable.configStatusTitle": "構成ステータス", - "xpack.beatsManagement.beatsTable.disenrollSelectedLabel": "選択項目の登録を解除", - "xpack.beatsManagement.beatsTable.failedStatusLabel": "エラー", - "xpack.beatsManagement.beatsTable.runningStatusLabel": "実行中", - "xpack.beatsManagement.beatsTable.startingStatusLabel": "開始中", - "xpack.beatsManagement.beatsTable.stoppedStatusLabel": "停止中", - "xpack.beatsManagement.beatsTable.tagsTitle": "タグ", - "xpack.beatsManagement.beatsTable.typeLabel": "型", - "xpack.beatsManagement.beatsTable.typeTitle": "型", - "xpack.beatsManagement.beatsTable.updatingStatusLabel": "更新中", - "xpack.beatsManagement.beatTagsTable.addTagLabel": "タグを追加", - "xpack.beatsManagement.beatTagsTable.lastUpdateTitle": "最終更新", - "xpack.beatsManagement.beatTagsTable.removeSelectedLabel": "選択項目を削除", - "xpack.beatsManagement.beatTagsTable.tagNameTitle": "タグ名", - "xpack.beatsManagement.breadcrumb.beatDetails": "{beatId} のビート詳細", - "xpack.beatsManagement.breadcrumb.beatTags": "{beatId} のビートタグ", - "xpack.beatsManagement.breadcrumb.configurationTags": "構成タグ", - "xpack.beatsManagement.breadcrumb.enrolledBeats": "登録済みのビート", - "xpack.beatsManagement.centralManagementLinkLabel": "Beatsの集中管理", - "xpack.beatsManagement.config.other.error": "有効な YAML フォーマットを使用してください", - "xpack.beatsManagement.config.otherConfigDescription": "YAML フォーマットで Filebeat インプットの他の設定を指定します", - "xpack.beatsManagement.config.otherConfigLabel": "他の構成", - "xpack.beatsManagement.confirmModal.cancelButtonLabel": "キャンセル", - "xpack.beatsManagement.confirmModal.confirmButtonLabel": "確認", - "xpack.beatsManagement.confirmModal.confirmWarningTitle": "確認", - "xpack.beatsManagement.createTag.errorSavingTagTitle": "タグの保存中にエラーが発生", - "xpack.beatsManagement.createTag.saveAndContinueButtonLabel": "保存して続行", - "xpack.beatsManagement.disabledSecurityDescription": "ビートの集中管理を使用するには、Kibana と Elasticsearch でセキュリティを有効にする必要があります。", - "xpack.beatsManagement.disabledSecurityTitle": "セキュリティが有効ではありません", - "xpack.beatsManagement.enrollBeat.assignTagToBeatInvalidURLNoTagFoundTitle": "無効な URL、createdTag が見つかりません", - "xpack.beatsManagement.enrollBeat.assignTagToBeatInvalidURLNoTokenFountTitle": "無効な URL、enrollmentToken が見つかりません", - "xpack.beatsManagement.enrollBeat.assignTagToBeatNotEnrolledProperlyTitle": "エラー:ビートは正常に登録されていません", - "xpack.beatsManagement.enrollBeat.beatEnrolledTitle": "このビートは現在集中管理に登録されています:", - "xpack.beatsManagement.enrollBeat.beatsCentralManagementDescription": "一か所で構成を集中管理できます。", - "xpack.beatsManagement.enrollBeat.beatTypeColumnName": "ビートタイプ", - "xpack.beatsManagement.enrollBeat.beatTypeTitle": "ビートタイプ:", - "xpack.beatsManagement.enrollBeat.copyButtonLabel": "コマンドをコピー", - "xpack.beatsManagement.enrollBeat.createTagStepLabel": "タグを作成", - "xpack.beatsManagement.enrollBeat.enrollBeatButtonLabel": "ビートを登録", - "xpack.beatsManagement.enrollBeat.enrollBeatStepLabel": "ビートを登録", - "xpack.beatsManagement.enrollBeat.finishStepLabel": "終了", - "xpack.beatsManagement.enrollBeat.firstBeatEnrollingDoneButtonLabel": "完了", - "xpack.beatsManagement.enrollBeat.getStartedBeatsCentralManagementTitle": "ビートの集中管理を開始", - "xpack.beatsManagement.enrollBeat.hostnameColumnName": "ホスト名", - "xpack.beatsManagement.enrollBeat.nextStepDescription": "ビートを起動して構成エラーを確認し、「完了」をクリックしてください。", - "xpack.beatsManagement.enrollBeat.nextStepTitle": "ビートが登録されました。次のステップ", - "xpack.beatsManagement.enrollBeat.platformTitle": "プラットフォーム:", - "xpack.beatsManagement.enrollBeat.versionColumnName": "バージョン", - "xpack.beatsManagement.enrollBeat.waitingBeatTypeToEnrollTitle": "{beatType} の登録待ち…", - "xpack.beatsManagement.enrollBeat.yourBeatTypeHostTitle": "{beatType} がインストールされているホストでは次を実行:", - "xpack.beatsManagement.filebeatInputConfig.otherConfigDescription": "YAML フォーマットで Filebeat インプットの他の設定を指定します", - "xpack.beatsManagement.filebeatInputConfig.otherConfigErrorMessage": "有効な YAML フォーマットを使用してください", - "xpack.beatsManagement.filebeatInputConfig.otherConfigLabel": "他の構成", - "xpack.beatsManagement.filebeatInputConfig.pathsDescription": "それぞれのパスを別々の行に入力します", - "xpack.beatsManagement.filebeatInputConfig.pathsErrorMessage": "1 行につき 1 つのパスを入力してください", - "xpack.beatsManagement.filebeatInputConfig.pathsLabel": "パス", - "xpack.beatsManagement.filebeatModuleConfig.moduleDescription": "YAML フォーマットで Filebeat モジュールの他の設定を指定します", - "xpack.beatsManagement.filebeatModuleConfig.moduleErrorMessage": "モジュールを選択してください。", - "xpack.beatsManagement.filebeatModuleConfig.moduleLabel": "モジュール", - "xpack.beatsManagement.filebeatModuleConfig.otherConfigErrorMessage": "有効な YAML フォーマットを使用してください", - "xpack.beatsManagement.filebeatModuleConfig.otherConfigLabel": "他の構成", - "xpack.beatsManagement.invalidLicenseDescription": "現在のライセンスは期限切れです。登録されたビートは引き続き動作しますが、ビート管理 UI にアクセスするには有効なライセンスが必要です。", - "xpack.beatsManagement.invalidLicenseTitle": "ライセンスの期限切れ", - "xpack.beatsManagement.management.breadcrumb": "管理", - "xpack.beatsManagement.management.deprecationMessage": "Beats集中管理の開発は中止され、現在はそれに代わる包括的なソリューションを検討しています。ベータ版に参加して、フィードバックを提出していただき、どうもありがとうございます。ご質問や問題点がありましたら、{forumLink}からお問い合わせください。", - "xpack.beatsManagement.management.deprecationTitle": "Beats集中管理は廃止予定です", - "xpack.beatsManagement.management.forumLink": "ディスカッションフォーラム", - "xpack.beatsManagement.metricbeatModuleConfig.hostsDescription": "それぞれのパスを別々の行に入力します", - "xpack.beatsManagement.metricbeatModuleConfig.hostsErrorMessage": "1 行につき 1 つのファイルホストを入力してください", - "xpack.beatsManagement.metricbeatModuleConfig.hostsLabel": "ホスト", - "xpack.beatsManagement.metricbeatModuleConfig.moduleErrorMessage": "モジュールを選択してください。", - "xpack.beatsManagement.metricbeatModuleConfig.moduleLabel": "モジュール", - "xpack.beatsManagement.metricbeatModuleConfig.otherConfigDescription": "YAML フォーマットで Metricbeat モジュールの他の設定を指定します", - "xpack.beatsManagement.metricbeatModuleConfig.otherConfigErrorMessage": "有効な YAML フォーマットを使用してください", - "xpack.beatsManagement.metricbeatModuleConfig.otherConfigLabel": "他の構成", - "xpack.beatsManagement.metricbeatModuleConfig.periodErrorMessage": "無効な期間、10 秒は「10s」というフォーマットを使用してください", - "xpack.beatsManagement.metricbeatModuleConfig.periodLabel": "期間", - "xpack.beatsManagement.noAccess.accessDeniedDescription": "ビート集中管理へのアクセス権がありません。ビート集中管理を使用するには、{beatsAdminRole} ロールから権限が必要です。", - "xpack.beatsManagement.noAccess.accessDeniedTitle": "アクセスが拒否されました", - "xpack.beatsManagement.noContentFoundErrorMessage": "コンテンツがありません", - "xpack.beatsManagement.outputConfig.hostsErrorMessage": "1 行につき 1 つのファイルホストを入力してください", - "xpack.beatsManagement.outputConfig.hostsLabel": "ホスト", - "xpack.beatsManagement.outputConfig.outputTypeErrorMessage": "アウトプットタイプを選択してください", - "xpack.beatsManagement.outputConfig.outputTypeLabel": "アウトプットタイプ", - "xpack.beatsManagement.outputConfig.passwordErrorMessage": "処理不能なパスワードです", - "xpack.beatsManagement.outputConfig.passwordLabel": "パスワード", - "xpack.beatsManagement.outputConfig.usernameErrorMessage": "処理不能なユーザー名です", - "xpack.beatsManagement.outputConfig.usernameLabel": "ユーザー名", - "xpack.beatsManagement.overview.betaBadgeText": "ベータ", - "xpack.beatsManagement.table.filterResultsPlaceholder": "フィルター結果", - "xpack.beatsManagement.table.selectOptionLabel": "オプションを選択してください", - "xpack.beatsManagement.table.selectThisBeatTooltip": "このビートを選択します", - "xpack.beatsManagement.tag.addConfigurationButtonLabel": "構成ブロックを追加", - "xpack.beatsManagement.tag.beatsAssignedToTagTitle": "このタグが付いたビート", - "xpack.beatsManagement.tag.cancelButtonLabel": "キャンセル", - "xpack.beatsManagement.tag.createTagTitle": "タグの作成", - "xpack.beatsManagement.tag.saveButtonLabel": "保存", - "xpack.beatsManagement.tag.tagColorLabel": "タグの色", - "xpack.beatsManagement.tag.tagConfigurationsDescription": "タグには異なるタイプのビートの構成ブロックが使用できます。たとえば、1 つのタグに 2 つの Metricbeat 構成ブロックと 1 つの Filebeat インプット構成ブロックを使用することができます。", - "xpack.beatsManagement.tag.tagConfigurationsTitle": "構成ブロック", - "xpack.beatsManagement.tag.tagDetailsDescription": "タグとは、1 つまたは複数のビートに適用できる構成ブロックのグループです。", - "xpack.beatsManagement.tag.tagDetailsTitle": "タグの詳細", - "xpack.beatsManagement.tag.tagName.validationErrorMessage": "タグ名には文字、数字、ハイフンのみが使用できます。", - "xpack.beatsManagement.tag.tagNameLabel": "タグ名", - "xpack.beatsManagement.tag.tagNamePlaceholder": "タグ名 (必須) ", - "xpack.beatsManagement.tag.updateTagTitle": "タグ {tagId} を更新", - "xpack.beatsManagement.tagConfig.addConfigurationTitle\"": "構成ブロックを追加", - "xpack.beatsManagement.tagConfig.closeButtonLabel": "閉じる", - "xpack.beatsManagement.tagConfig.configurationTypeText": "{configType}‘構成", - "xpack.beatsManagement.tagConfig.descriptionLabel": "説明", - "xpack.beatsManagement.tagConfig.descriptionPlaceholder": "説明 (オプション) ", - "xpack.beatsManagement.tagConfig.editConfigurationTitle": "構成ブロックの編集", - "xpack.beatsManagement.tagConfig.filebeatInputLabel": "Filebeat インプット", - "xpack.beatsManagement.tagConfig.filebeatModuleLabel": "Filebeat モジュール", - "xpack.beatsManagement.tagConfig.invalidSchema": "エラー:この構成は無効です。Beats でサポートされておらず、削除する必要があります", - "xpack.beatsManagement.tagConfig.metricbeatModuleLabel": "Metricbeat モジュール", - "xpack.beatsManagement.tagConfig.outputLabel": "アウトプット", - "xpack.beatsManagement.tagConfig.saveButtonLabel": "保存", - "xpack.beatsManagement.tagConfig.typeLabel": "型", - "xpack.beatsManagement.tagConfig.viewConfigurationTitle\"": "構成ブロックの表示", - "xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsButtonLabel": "タグを削除", - "xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsWarninigMessage": "選択されたビートからタグを削除しますか?", - "xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsWarninigTitle": "タグを削除", - "xpack.beatsManagement.tagListAssignmentOptions.removeTagsButtonLabel": "選択項目を削除", - "xpack.beatsManagement.tagListAssignmentOptions.removeTagsWarninigTitle": "タグを削除", - "xpack.beatsManagement.tagListAssignmentOptions.removeTagWarninigMessage": "タグを削除しますか?", - "xpack.beatsManagement.tags.addTagButtonLabel": "タグを追加", - "xpack.beatsManagement.tags.someTagsMightBeAssignedToBeatsTitle": "これらのタグのいくつかはビートに割り当てられている可能性があります。削除するタグが現在割り当てられていないことを確認してください", - "xpack.beatsManagement.tagsTable.lastUpdateTitle": "最終更新", - "xpack.beatsManagement.tagsTable.removeSelectedLabel": "選択項目を削除", - "xpack.beatsManagement.tagsTable.tagNameTitle": "タグ名", - "xpack.beatsManagement.tagTable.actions.removeButtonAriaLabel": "削除", - "xpack.beatsManagement.tagTable.actions.removeTooltip": "タグからこの構成を削除します", - "xpack.beatsManagement.tagTable.actionsColumnName": "アクション", - "xpack.beatsManagement.tagTable.descriptionColumnName": "説明", - "xpack.beatsManagement.tagTable.moduleColumn.notAvailibaleLabel": "N/A", - "xpack.beatsManagement.tagTable.moduleColumnName": "モジュール", - "xpack.beatsManagement.tagTable.typeColumnName": "型", - "xpack.beatsManagement.walkthrough.initial.betaBadgeText": "ベータ", "xpack.canvas.app.loadErrorMessage": "メッセージ:{error}", "xpack.canvas.app.loadErrorTitle": "Canvas の読み込みに失敗", "xpack.canvas.app.loadingMessage": "Canvas を読み込み中", @@ -9669,7 +9490,7 @@ "xpack.fleet.settings.cancelButtonLabel": "キャンセル", "xpack.fleet.settings.elasticHostError": "無効なURL", "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearchホスト", - "xpack.fleet.settings.elasticsearchUrlsHelpTect": "エージェントがデータを送信するElasticsearch URLを指定します。", + "xpack.fleet.settings.elasticsearchUrlsHelpTect": "エージェントがデータを送信するElasticsearch URLを指定します。{link}を参照してください。", "xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError": "各URLのプロトコルとパスは同じでなければなりません", "xpack.fleet.settings.fleetServerHostsEmptyError": "1つ以上のURLが必要です。", "xpack.fleet.settings.fleetServerHostsError": "無効なURL", @@ -17836,7 +17657,6 @@ "xpack.observability.home.getStatedButton": "使ってみる", "xpack.observability.home.sectionsubtitle": "ログ、メトリック、トレースを大規模に、1つのスタックにまとめて、環境内のあらゆる場所で生じるイベントの監視、分析、対応を行います。", "xpack.observability.home.sectionTitle": "エコシステム全体の一元的な可視性", - "xpack.observability.home.title": "オブザーバビリティ", "xpack.observability.landing.breadcrumb": "はじめて使う", "xpack.observability.news.readFullStory": "詳細なストーリーを読む", "xpack.observability.news.title": "新機能", @@ -23651,19 +23471,6 @@ "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "ルールを更新できません", "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "'{ruleName}'を更新しました", - "xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "削除", - "xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "このアクションは無効です", - "xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} コネクター", - "xpack.triggersActionsUI.sections.alertForm.actionRunWhenInActionGroup": "次のときに実行", - "xpack.triggersActionsUI.sections.alertForm.actionSectionsTitle": "アクション", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByConfigMessageTitle": "この機能は Kibana の構成で無効になっています。", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseLinkTitle": "ライセンスオプションを表示", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageDescription": "このアクションを再び有効にするには、ライセンスをアップグレードしてください。", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageTitle": "この機能には {minimumLicenseRequired} ライセンスが必要です。", - "xpack.triggersActionsUI.sections.alertForm.addActionButtonLabel": "アクションの追加", - "xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel": "コネクターを作成する", - "xpack.triggersActionsUI.sections.alertForm.addNewActionConnectorActionGroup.display": "{actionGroupName} (現在サポートされていません) ", - "xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton": "コネクターの追加", "xpack.triggersActionsUI.sections.alertForm.alertNameLabel": "名前", "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.label": "毎", "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.description": "アラートステータスが変更されるときにアクションを実行します。", @@ -23681,37 +23488,22 @@ "xpack.triggersActionsUI.sections.alertForm.conditions.addConditionLabel": "追加:", "xpack.triggersActionsUI.sections.alertForm.conditions.removeConditionLabel": "削除", "xpack.triggersActionsUI.sections.alertForm.conditions.title": "条件:", - "xpack.triggersActionsUI.sections.alertForm.connectorAddInline.actionIdLabel": "別の{connectorInstance}コネクターを使用", - "xpack.triggersActionsUI.sections.alertForm.connectorAddInline.addNewConnectorEmptyButton": "コネクターの追加", "xpack.triggersActionsUI.sections.alertForm.documentationLabel": "ドキュメント", - "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "{actionTypeName}コネクターがありません", "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypes": "ルールを{operation}するには、適切な権限が付与されている必要があります。", "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypesTitle": "ルールタイプを{operation}する権限がありません。", "xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector": "{actionTypeId}コネクターのアクションが必要です。", "xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText": "確認間隔が必要です。", "xpack.triggersActionsUI.sections.alertForm.error.requiredNameText": "名前が必要です。", "xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText": "ルールタイプは必須です。", - "xpack.triggersActionsUI.sections.alertForm.existingAlertActionTypeEditTitle": "{actionConnectorName}", - "xpack.triggersActionsUI.sections.alertForm.loadingConnectorsDescription": "コネクターを読み込んでいます…", - "xpack.triggersActionsUI.sections.alertForm.loadingConnectorTypesDescription": "コネクタータイプを読み込んでいます...", "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypeParamsDescription": "ルールタイプパラメーターを読み込んでいます…", "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypesDescription": "ルールタイプを読み込んでいます…", - "xpack.triggersActionsUI.sections.alertForm.newAlertActionTypeEditTitle": "{actionConnectorName}", - "xpack.triggersActionsUI.sections.alertForm.preconfiguredTitleMessage": "構成済み", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "ルールがアクティブな間にアクションを繰り返す頻度を定義します。", "xpack.triggersActionsUI.sections.alertForm.ruleTypeSelectLabel": "ルールタイプを選択", "xpack.triggersActionsUI.sections.alertForm.searchPlaceholderTitle": "検索", - "xpack.triggersActionsUI.sections.alertForm.selectConnectorTypeTitle": "コネクタータイプを選択", "xpack.triggersActionsUI.sections.alertForm.solutionFilterLabel": "ユースケースでフィルタリング", "xpack.triggersActionsUI.sections.alertForm.tagsFieldLabel": "タグ (任意) ", - "xpack.triggersActionsUI.sections.alertForm.unableToAddAction": "デフォルトアクショングループの定義がないのでアクションを追加できません", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage": "コネクターを読み込めません", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle": "コネクターを読み込めません。", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle'": "コネクターを読み込めません。", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTypesMessage": "コネクタータイプを読み込めません", "xpack.triggersActionsUI.sections.alertForm.unableToLoadRuleTypesMessage": "ルールタイプを読み込めません", - "xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors": "許可されたユーザーのみがコネクターを構成できます。管理者にお問い合わせください。", "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "アクションタイプ", "xpack.triggersActionsUI.sections.alertsList.addRuleButtonLabel": "ルールを作成", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDecrypting": "ルールの復号中にエラーが発生しました。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 233bafeb71f3aa..a7cd8b5fe8d515 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4241,7 +4241,7 @@ "visDefaultEditor.controls.ranges.fromLabel": "自", "visDefaultEditor.controls.ranges.greaterThanOrEqualPrepend": "≥", "visDefaultEditor.controls.ranges.greaterThanOrEqualTooltip": "大于或等于", - "visDefaultEditor.controls.ranges.lessThanPrepend": "<", + "visDefaultEditor.controls.ranges.lessThanPrepend": "<", "visDefaultEditor.controls.ranges.lessThanTooltip": "小于", "visDefaultEditor.controls.ranges.removeRangeButtonAriaLabel": "移除范围 {from} 至 {to}", "visDefaultEditor.controls.ranges.toLabel": "至", @@ -6195,189 +6195,6 @@ "xpack.banners.settings.textColor.description": "设置横幅广告文本的颜色。{subscriptionLink}", "xpack.banners.settings.textColor.title": "横幅广告文本颜色", "xpack.banners.settings.textContent.title": "横幅广告文本", - "xpack.beatsManagement.beat.actionSectionTypeLabel": "类型:{beatType}。", - "xpack.beatsManagement.beat.actionSectionVersionLabel": "版本:{beatVersion}。", - "xpack.beatsManagement.beat.beatNameAndIdTitle": "Beat:{nameOrNoName} (ID:{id}) ", - "xpack.beatsManagement.beat.beatNotFoundErrorMessage": "未找到 Beat", - "xpack.beatsManagement.beat.beatNotFoundErrorTitle": "未找到 Beat", - "xpack.beatsManagement.beat.beatNotFoundMessage": "未找到 Beat", - "xpack.beatsManagement.beat.configTabLabel": "配置", - "xpack.beatsManagement.beat.configurationTagsTabLabel": "配置标签", - "xpack.beatsManagement.beat.detailsConfigurationDescription": "可以将多个配置应用到单个标签。这些配置类型可以根据需要进行重复或混合。例如,您可以同时使用三个 Metricbeat 配置以及一个输入和 Filebeat 配置。", - "xpack.beatsManagement.beat.detailsConfigurationTitle": "配置", - "xpack.beatsManagement.beat.lastConfigUpdateMessage": "上次配置更新:{lastUpdateTime}。", - "xpack.beatsManagement.beat.loadingTitle": "正在加载", - "xpack.beatsManagement.beatConfigurations.descriptionColumnName": "描述", - "xpack.beatsManagement.beatConfigurations.moduleColumnName": "模块", - "xpack.beatsManagement.beatConfigurations.tagColumnName": "标签", - "xpack.beatsManagement.beatConfigurations.typeColumnName": "类型", - "xpack.beatsManagement.beats.addedNotificationDescription": "将标签 {tag} 添加到 {assignmentsLength, plural, one {Beat {beatName}} other {# 个 Beats}}。", - "xpack.beatsManagement.beats.addedNotificationTitle": "{assignmentsLength, plural, other {标签}}已添加", - "xpack.beatsManagement.beats.beatDisenrolledNotificationDescription": "ID 为 {firstBeatId} 的 Beat 已除名。", - "xpack.beatsManagement.beats.beatDisenrolledNotificationTitle": "{firstBeatNameOrId} 已除名", - "xpack.beatsManagement.beats.configurationTagsTabTitle": "配置标签", - "xpack.beatsManagement.beats.disenrolledBeatsNotificationTitle": "{beatsLength} 个 Beats 已除名", - "xpack.beatsManagement.beats.enrollBeatsButtonLabel": "注册 Beats", - "xpack.beatsManagement.beats.enrolledBeatsTabTitle": "已注册 Beats", - "xpack.beatsManagement.beats.enrollNewBeatsTitle": "注册新 Beat", - "xpack.beatsManagement.beats.installBeatsLearningButtonLabel": "了解如何安装 Beats", - "xpack.beatsManagement.beats.removedNotificationDescription": "已从 {assignmentsLength, plural, one {Beat {beatName}} other {# 个 Beat}} 移除标签 {tag}", - "xpack.beatsManagement.beats.removedNotificationTitle": "{assignmentsLength, plural, other {标签}}已删除", - "xpack.beatsManagement.beatsListAssignmentOptions.setTagsButtonLabel": "设置标签", - "xpack.beatsManagement.beatsListAssignmentOptions.unenrollBeatsWarninigMessage": "选定的 Beats 将不再使用集中管理", - "xpack.beatsManagement.beatsListAssignmentOptions.unenrollBeatsWarninigTitle": "取消注册选定的 Beats?", - "xpack.beatsManagement.beatsListAssignmentOptions.unenrollButtonLabel": "取消注册选定", - "xpack.beatsManagement.beatsTable.beatNameTitle": "Beat 名称", - "xpack.beatsManagement.beatsTable.configErrorStatusLabel": "配置错误", - "xpack.beatsManagement.beatsTable.configStatus.errorTooltip": "此 Beat 有错误,请查看此主机的日志。", - "xpack.beatsManagement.beatsTable.configStatus.noConnectionTooltip": "此 Beat 未连接到 Kibana 的时间已超过 10 分钟", - "xpack.beatsManagement.beatsTable.configStatus.notStartedLabel": "未开始", - "xpack.beatsManagement.beatsTable.configStatus.notStartedTooltip": "此 Beat 尚未启动。", - "xpack.beatsManagement.beatsTable.configStatus.offlineLabel": "脱机", - "xpack.beatsManagement.beatsTable.configStatus.okLabel": "确定", - "xpack.beatsManagement.beatsTable.configStatus.okTooltip": "Beat 成功应用最新的配置", - "xpack.beatsManagement.beatsTable.configStatus.progressTooltip": "此 Beat 当前正在从 CM 重新加载配置。", - "xpack.beatsManagement.beatsTable.configStatus.runningTooltip": "此 Beat 正在运行,没有任何问题。", - "xpack.beatsManagement.beatsTable.configStatus.startingTooltip": "此 Beat 正在启动。", - "xpack.beatsManagement.beatsTable.configStatusTitle": "配置状态", - "xpack.beatsManagement.beatsTable.disenrollSelectedLabel": "取消注册选定", - "xpack.beatsManagement.beatsTable.failedStatusLabel": "错误", - "xpack.beatsManagement.beatsTable.runningStatusLabel": "正在运行", - "xpack.beatsManagement.beatsTable.startingStatusLabel": "正在启动", - "xpack.beatsManagement.beatsTable.stoppedStatusLabel": "已停止", - "xpack.beatsManagement.beatsTable.tagsTitle": "标签", - "xpack.beatsManagement.beatsTable.typeLabel": "类型", - "xpack.beatsManagement.beatsTable.typeTitle": "类型", - "xpack.beatsManagement.beatsTable.updatingStatusLabel": "正在更新", - "xpack.beatsManagement.beatTagsTable.addTagLabel": "添加标签", - "xpack.beatsManagement.beatTagsTable.lastUpdateTitle": "上次更新", - "xpack.beatsManagement.beatTagsTable.removeSelectedLabel": "删除选定", - "xpack.beatsManagement.beatTagsTable.tagNameTitle": "标签名称", - "xpack.beatsManagement.breadcrumb.beatDetails": "{beatId} 的 Beat 详情", - "xpack.beatsManagement.breadcrumb.beatTags": "{beatId} 的 Beat 标签", - "xpack.beatsManagement.breadcrumb.configurationTags": "配置标签", - "xpack.beatsManagement.breadcrumb.enrolledBeats": "已注册 Beats", - "xpack.beatsManagement.centralManagementLinkLabel": "Beats 集中管理", - "xpack.beatsManagement.config.other.error": "使用有效的 YAML 格式", - "xpack.beatsManagement.config.otherConfigDescription": "使用 YAML 格式指定 Filebeat 输入的其他设置", - "xpack.beatsManagement.config.otherConfigLabel": "其他配置", - "xpack.beatsManagement.confirmModal.cancelButtonLabel": "取消", - "xpack.beatsManagement.confirmModal.confirmButtonLabel": "确认", - "xpack.beatsManagement.confirmModal.confirmWarningTitle": "确认", - "xpack.beatsManagement.createTag.errorSavingTagTitle": "保存标签时出错", - "xpack.beatsManagement.createTag.saveAndContinueButtonLabel": "保存并继续", - "xpack.beatsManagement.disabledSecurityDescription": "必须在 Kibana 和 Elasticsearch 启用安全性,才能使用 Beats 集中管理。", - "xpack.beatsManagement.disabledSecurityTitle": "安全性未启用", - "xpack.beatsManagement.enrollBeat.assignTagToBeatInvalidURLNoTagFoundTitle": "URL 无效,未找到 createdTag", - "xpack.beatsManagement.enrollBeat.assignTagToBeatInvalidURLNoTokenFountTitle": "URL 无效,未找到 enrollmentToken", - "xpack.beatsManagement.enrollBeat.assignTagToBeatNotEnrolledProperlyTitle": "错误:Beat 未正确注册", - "xpack.beatsManagement.enrollBeat.beatEnrolledTitle": "该 Beat 当前已在集中管理中注册:", - "xpack.beatsManagement.enrollBeat.beatsCentralManagementDescription": "在集中位置管理您的配置。", - "xpack.beatsManagement.enrollBeat.beatTypeColumnName": "Beat 类型", - "xpack.beatsManagement.enrollBeat.beatTypeTitle": "Beat 类型:", - "xpack.beatsManagement.enrollBeat.copyButtonLabel": "复制命令", - "xpack.beatsManagement.enrollBeat.createTagStepLabel": "创建标签", - "xpack.beatsManagement.enrollBeat.enrollBeatButtonLabel": "注册 Beat", - "xpack.beatsManagement.enrollBeat.enrollBeatStepLabel": "注册 Beat", - "xpack.beatsManagement.enrollBeat.finishStepLabel": "完成", - "xpack.beatsManagement.enrollBeat.firstBeatEnrollingDoneButtonLabel": "完成", - "xpack.beatsManagement.enrollBeat.getStartedBeatsCentralManagementTitle": "开始使用 Beats 集中管理", - "xpack.beatsManagement.enrollBeat.hostnameColumnName": "主机名", - "xpack.beatsManagement.enrollBeat.nextStepDescription": "启动您的 Beat 以检查是否有配置错误,然后单击“完成”。", - "xpack.beatsManagement.enrollBeat.nextStepTitle": "您的 Beat 已注册。后续操作", - "xpack.beatsManagement.enrollBeat.platformTitle": "平台:", - "xpack.beatsManagement.enrollBeat.versionColumnName": "版本", - "xpack.beatsManagement.enrollBeat.waitingBeatTypeToEnrollTitle": "正在等待注册 {beatType}……", - "xpack.beatsManagement.enrollBeat.yourBeatTypeHostTitle": "在安装您的 {beatType} 的主机上,运行:", - "xpack.beatsManagement.filebeatInputConfig.otherConfigDescription": "使用 YAML 格式指定 Filebeat 输入的其他设置", - "xpack.beatsManagement.filebeatInputConfig.otherConfigErrorMessage": "使用有效的 YAML 格式", - "xpack.beatsManagement.filebeatInputConfig.otherConfigLabel": "其他配置", - "xpack.beatsManagement.filebeatInputConfig.pathsDescription": "将每个路径放置在单独的行上", - "xpack.beatsManagement.filebeatInputConfig.pathsErrorMessage": "每行一个文件路径", - "xpack.beatsManagement.filebeatInputConfig.pathsLabel": "路径", - "xpack.beatsManagement.filebeatModuleConfig.moduleDescription": "使用 YAML 格式指定 Filebeat 模块的其他设置", - "xpack.beatsManagement.filebeatModuleConfig.moduleErrorMessage": "请选择模块", - "xpack.beatsManagement.filebeatModuleConfig.moduleLabel": "模块", - "xpack.beatsManagement.filebeatModuleConfig.otherConfigErrorMessage": "使用有效的 YAML 格式", - "xpack.beatsManagement.filebeatModuleConfig.otherConfigLabel": "其他配置", - "xpack.beatsManagement.invalidLicenseDescription": "您当前的许可证已过期。已注册的 Beats 将继续工作,但您需要有效的许可,才能访问 Beats 管理 UI。", - "xpack.beatsManagement.invalidLicenseTitle": "已过期许可证", - "xpack.beatsManagement.management.breadcrumb": "管理", - "xpack.beatsManagement.management.deprecationMessage": "我们已停止开发 Beats 集中管理,正开发一款全面的解决方案来替代它。感谢您参与公测版测试并提供反馈。如果您有任何问题或顾虑,请在 {forumLink} 上联系我们。", - "xpack.beatsManagement.management.deprecationTitle": "Beats 集中管理已弃用", - "xpack.beatsManagement.management.forumLink": "讨论论坛", - "xpack.beatsManagement.metricbeatModuleConfig.hostsDescription": "将每个路径放置在单独的行上", - "xpack.beatsManagement.metricbeatModuleConfig.hostsErrorMessage": "每行一个文件主机", - "xpack.beatsManagement.metricbeatModuleConfig.hostsLabel": "主机", - "xpack.beatsManagement.metricbeatModuleConfig.moduleErrorMessage": "请选择模块", - "xpack.beatsManagement.metricbeatModuleConfig.moduleLabel": "模块", - "xpack.beatsManagement.metricbeatModuleConfig.otherConfigDescription": "使用 YAML 格式指定 Metricbeat 模块的其他设置", - "xpack.beatsManagement.metricbeatModuleConfig.otherConfigErrorMessage": "使用有效的 YAML 格式", - "xpack.beatsManagement.metricbeatModuleConfig.otherConfigLabel": "其他配置", - "xpack.beatsManagement.metricbeatModuleConfig.periodErrorMessage": "期间无效,10 秒的格式必须为 `10s`。", - "xpack.beatsManagement.metricbeatModuleConfig.periodLabel": "期间", - "xpack.beatsManagement.noAccess.accessDeniedDescription": "您无权访问 Beats 集中管理。要使用 Beats 集中管理,您需要 {beatsAdminRole} 角色授予的权限。", - "xpack.beatsManagement.noAccess.accessDeniedTitle": "访问被拒绝", - "xpack.beatsManagement.noContentFoundErrorMessage": "未找到任何内容", - "xpack.beatsManagement.outputConfig.hostsErrorMessage": "每行一个文件主机", - "xpack.beatsManagement.outputConfig.hostsLabel": "主机", - "xpack.beatsManagement.outputConfig.outputTypeErrorMessage": "请选择输出类型", - "xpack.beatsManagement.outputConfig.outputTypeLabel": "输出类型", - "xpack.beatsManagement.outputConfig.passwordErrorMessage": "无法处理的密码", - "xpack.beatsManagement.outputConfig.passwordLabel": "密码", - "xpack.beatsManagement.outputConfig.usernameErrorMessage": "无法处理的用户名", - "xpack.beatsManagement.outputConfig.usernameLabel": "用户名", - "xpack.beatsManagement.overview.betaBadgeText": "公测版", - "xpack.beatsManagement.table.filterResultsPlaceholder": "筛选结果", - "xpack.beatsManagement.table.selectOptionLabel": "请选择选项", - "xpack.beatsManagement.table.selectThisBeatTooltip": "选择此 Beat", - "xpack.beatsManagement.tag.addConfigurationButtonLabel": "添加配置块", - "xpack.beatsManagement.tag.beatsAssignedToTagTitle": "具有此标签的 Beats", - "xpack.beatsManagement.tag.cancelButtonLabel": "取消", - "xpack.beatsManagement.tag.createTagTitle": "创建标签", - "xpack.beatsManagement.tag.saveButtonLabel": "保存", - "xpack.beatsManagement.tag.tagColorLabel": "标签颜色", - "xpack.beatsManagement.tag.tagConfigurationsDescription": "标签可以有不同类型 Beats 的配置块。例如,标签可以有两个 Metricbeat 配置块和一个 Filebeat 输入配置块。", - "xpack.beatsManagement.tag.tagConfigurationsTitle": "配置块", - "xpack.beatsManagement.tag.tagDetailsDescription": "标签是可以应用到一个或多个 Beats 的一组配置块。", - "xpack.beatsManagement.tag.tagDetailsTitle": "标签详情", - "xpack.beatsManagement.tag.tagName.validationErrorMessage": "标签名称只能由字母、数字和短划线构成", - "xpack.beatsManagement.tag.tagNameLabel": "标签名称", - "xpack.beatsManagement.tag.tagNamePlaceholder": "标签名称 (必填) ", - "xpack.beatsManagement.tag.updateTagTitle": "创建标签:{tagId}", - "xpack.beatsManagement.tagConfig.addConfigurationTitle\"": "添加配置块", - "xpack.beatsManagement.tagConfig.closeButtonLabel": "关闭", - "xpack.beatsManagement.tagConfig.configurationTypeText": "{configType} 配置", - "xpack.beatsManagement.tagConfig.descriptionLabel": "描述", - "xpack.beatsManagement.tagConfig.descriptionPlaceholder": "描述 (可选) ", - "xpack.beatsManagement.tagConfig.editConfigurationTitle": "编辑配置块", - "xpack.beatsManagement.tagConfig.filebeatInputLabel": "Filebeat 输入", - "xpack.beatsManagement.tagConfig.filebeatModuleLabel": "Filebeat 模块", - "xpack.beatsManagement.tagConfig.invalidSchema": "错误:此配置无效,其不受 Beats 支持,应移除", - "xpack.beatsManagement.tagConfig.metricbeatModuleLabel": "Metricbeat 模块", - "xpack.beatsManagement.tagConfig.outputLabel": "输出", - "xpack.beatsManagement.tagConfig.saveButtonLabel": "保存", - "xpack.beatsManagement.tagConfig.typeLabel": "类型", - "xpack.beatsManagement.tagConfig.viewConfigurationTitle\"": "查看配置块", - "xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsButtonLabel": "删除标签", - "xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsWarninigMessage": "从选定 Beats 删除该标签?", - "xpack.beatsManagement.tagConfigAssignmentOptions.removeTagsWarninigTitle": "删除标签", - "xpack.beatsManagement.tagListAssignmentOptions.removeTagsButtonLabel": "删除选定", - "xpack.beatsManagement.tagListAssignmentOptions.removeTagsWarninigTitle": "删除标签", - "xpack.beatsManagement.tagListAssignmentOptions.removeTagWarninigMessage": "删除标签?", - "xpack.beatsManagement.tags.addTagButtonLabel": "添加标签", - "xpack.beatsManagement.tags.someTagsMightBeAssignedToBeatsTitle": "以下部分标签可能已分配给 Beats。请确保正要删除的标签当前未被分配", - "xpack.beatsManagement.tagsTable.lastUpdateTitle": "上次更新", - "xpack.beatsManagement.tagsTable.removeSelectedLabel": "删除选定", - "xpack.beatsManagement.tagsTable.tagNameTitle": "标签名称", - "xpack.beatsManagement.tagTable.actions.removeButtonAriaLabel": "移除", - "xpack.beatsManagement.tagTable.actions.removeTooltip": "从标签删除此配置", - "xpack.beatsManagement.tagTable.actionsColumnName": "操作", - "xpack.beatsManagement.tagTable.descriptionColumnName": "描述", - "xpack.beatsManagement.tagTable.moduleColumn.notAvailibaleLabel": "不可用", - "xpack.beatsManagement.tagTable.moduleColumnName": "模块", - "xpack.beatsManagement.tagTable.typeColumnName": "类型", - "xpack.beatsManagement.walkthrough.initial.betaBadgeText": "公测版", "xpack.canvas.app.loadErrorMessage": "消息:{error}", "xpack.canvas.app.loadErrorTitle": "Canvas 加载失败", "xpack.canvas.app.loadingMessage": "Canvas 正在加载", @@ -9758,7 +9575,7 @@ "xpack.fleet.settings.cancelButtonLabel": "取消", "xpack.fleet.settings.elasticHostError": "URL 无效", "xpack.fleet.settings.elasticsearchUrlLabel": "Elasticsearch 主机", - "xpack.fleet.settings.elasticsearchUrlsHelpTect": "指定代理用于发送数据的 Elasticsearch URL。", + "xpack.fleet.settings.elasticsearchUrlsHelpTect": "指定代理用于发送数据的 Elasticsearch URL。请参阅 {link}。", "xpack.fleet.settings.fleetServerHostsDifferentPathOrProtocolError": "对于每个 URL,协议和路径必须相同", "xpack.fleet.settings.fleetServerHostsEmptyError": "至少需要一个 URL", "xpack.fleet.settings.fleetServerHostsError": "URL 无效", @@ -18077,7 +17894,6 @@ "xpack.observability.home.getStatedButton": "开始使用", "xpack.observability.home.sectionsubtitle": "通过根据需要将日志、指标和跟踪都置于单个堆栈上,来监测、分析和响应环境中任何位置发生的事件。", "xpack.observability.home.sectionTitle": "整个生态系统的统一可见性", - "xpack.observability.home.title": "可观测性", "xpack.observability.landing.breadcrumb": "入门", "xpack.observability.news.readFullStory": "详细了解", "xpack.observability.news.title": "最新动态", @@ -24016,19 +23832,6 @@ "xpack.triggersActionsUI.sections.alertEdit.saveButtonLabel": "保存", "xpack.triggersActionsUI.sections.alertEdit.saveErrorNotificationText": "无法更新规则。", "xpack.triggersActionsUI.sections.alertEdit.saveSuccessNotificationText": "已更新“{ruleName}”", - "xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel": "删除", - "xpack.triggersActionsUI.sections.alertForm.actionDisabledTitle": "此操作已禁用", - "xpack.triggersActionsUI.sections.alertForm.actionIdLabel": "{connectorInstance} 连接器", - "xpack.triggersActionsUI.sections.alertForm.actionRunWhenInActionGroup": "运行条件", - "xpack.triggersActionsUI.sections.alertForm.actionSectionsTitle": "操作", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByConfigMessageTitle": "此功能已由 Kibana 配置禁用。", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseLinkTitle": "查看许可证选项", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageDescription": "要重新启用此操作,请升级您的许可证。", - "xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageTitle": "此功能需要{minimumLicenseRequired}许可证。", - "xpack.triggersActionsUI.sections.alertForm.addActionButtonLabel": "添加操作", - "xpack.triggersActionsUI.sections.alertForm.addConnectorButtonLabel": "创建连接器", - "xpack.triggersActionsUI.sections.alertForm.addNewActionConnectorActionGroup.display": "{actionGroupName} (当前不支持) ", - "xpack.triggersActionsUI.sections.alertForm.addNewConnectorEmptyButton": "添加连接器", "xpack.triggersActionsUI.sections.alertForm.alertNameLabel": "名称", "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.label": "每", "xpack.triggersActionsUI.sections.alertForm.alertNotifyWhen.onActionGroupChange.description": "操作在告警状态更改时运行。", @@ -24046,37 +23849,22 @@ "xpack.triggersActionsUI.sections.alertForm.conditions.addConditionLabel": "添加:", "xpack.triggersActionsUI.sections.alertForm.conditions.removeConditionLabel": "移除", "xpack.triggersActionsUI.sections.alertForm.conditions.title": "条件:", - "xpack.triggersActionsUI.sections.alertForm.connectorAddInline.actionIdLabel": "使用其他 {connectorInstance} 连接器", - "xpack.triggersActionsUI.sections.alertForm.connectorAddInline.addNewConnectorEmptyButton": "添加连接器", "xpack.triggersActionsUI.sections.alertForm.documentationLabel": "文档", - "xpack.triggersActionsUI.sections.alertForm.emptyConnectorsLabel": "无 {actionTypeName} 连接器", "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypes": "为了{operation}规则,您需要获得相应的权限。", "xpack.triggersActionsUI.sections.alertForm.error.noAuthorizedRuleTypesTitle": "您尚无权{operation}任何规则类型", "xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector": "“{actionTypeId} 连接器的操作”必填。", "xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText": "“检查时间间隔”必填。", "xpack.triggersActionsUI.sections.alertForm.error.requiredNameText": "“名称”必填。", "xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText": "“规则类型”必填。", - "xpack.triggersActionsUI.sections.alertForm.existingAlertActionTypeEditTitle": "{actionConnectorName}", - "xpack.triggersActionsUI.sections.alertForm.loadingConnectorsDescription": "正在加载连接器……", - "xpack.triggersActionsUI.sections.alertForm.loadingConnectorTypesDescription": "正在加载连接器类型……", "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypeParamsDescription": "正在加载规则类型参数……", "xpack.triggersActionsUI.sections.alertForm.loadingRuleTypesDescription": "正在加载规则类型……", - "xpack.triggersActionsUI.sections.alertForm.newAlertActionTypeEditTitle": "{actionConnectorName}", - "xpack.triggersActionsUI.sections.alertForm.preconfiguredTitleMessage": "预配置", "xpack.triggersActionsUI.sections.alertForm.renotifyFieldLabel": "通知", "xpack.triggersActionsUI.sections.alertForm.renotifyWithTooltip": "定义规则处于活动状态时重复操作的频率。", "xpack.triggersActionsUI.sections.alertForm.ruleTypeSelectLabel": "选择规则类型", "xpack.triggersActionsUI.sections.alertForm.searchPlaceholderTitle": "搜索", - "xpack.triggersActionsUI.sections.alertForm.selectConnectorTypeTitle": "选择连接器类型", "xpack.triggersActionsUI.sections.alertForm.solutionFilterLabel": "按用例筛选", "xpack.triggersActionsUI.sections.alertForm.tagsFieldLabel": "标签 (可选) ", - "xpack.triggersActionsUI.sections.alertForm.unableToAddAction": "无法添加操作,因为未定义默认操作组", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage": "无法加载连接器", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle": "无法加载连接器。", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTitle'": "无法加载连接器。", - "xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTypesMessage": "无法加载连接器类型", "xpack.triggersActionsUI.sections.alertForm.unableToLoadRuleTypesMessage": "无法加载规则类型", - "xpack.triggersActionsUI.sections.alertForm.unauthorizedToCreateForEmptyConnectors": "只有获得授权的用户才能配置连接器。请联系您的管理员。", "xpack.triggersActionsUI.sections.alertsList.actionTypeFilterLabel": "操作类型", "xpack.triggersActionsUI.sections.alertsList.addRuleButtonLabel": "创建规则", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonDecrypting": "解密规则时发生错误。", diff --git a/x-pack/plugins/triggers_actions_ui/README.md b/x-pack/plugins/triggers_actions_ui/README.md index dc1da533af374b..7d736218af2d99 100644 --- a/x-pack/plugins/triggers_actions_ui/README.md +++ b/x-pack/plugins/triggers_actions_ui/README.md @@ -206,13 +206,13 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent + <> {hasExpressionErrors ? ( - + <> - + ) : null} @@ -221,7 +221,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent .... - + ); }; @@ -322,7 +322,7 @@ Fields of this object `AlertTypeModel` will be mapped properly in the UI below. 2. Define `alertParamsExpression` as `React.FunctionComponent` - this is the form for filling Alert params based on the current Alert type. ``` -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { WhenExpression, OfExpression } from '../../../../common/expression_items'; import { builtInAggregationTypes } from '../../../../common/constants'; @@ -340,7 +340,7 @@ export const ExampleExpression: React.FunctionComponent = ({ }) => { const [aggType, setAggType] = useState('count'); return ( - + <> = ({ ) : null} - + ); }; @@ -653,7 +653,7 @@ const ThresholdSpecifier = ( }) => { if (!actionGroup) { // render empty if no condition action group is specified - return ; + return null; } return ( @@ -1073,7 +1073,7 @@ Action type model definition: export function getActionType(): ActionTypeModel { return { id: '.pagerduty', - iconClass: 'apps', + iconClass: lazy(() => import('./logo')), selectMessage: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText', { @@ -1110,7 +1110,7 @@ and action params form available in Create Alert form: Each action type should be defined as an `ActionTypeModel` object with the following properties: ``` id: string; - iconClass: string; + iconClass: IconType; selectMessage: string; actionTypeTitle?: string; validateConnector: (connector: any) => ValidationResult; @@ -1121,7 +1121,7 @@ Each action type should be defined as an `ActionTypeModel` object with the follo |Property|Description| |---|---| |id|Action type id. Should be the same as on server side.| -|iconClass|Icon of action type, that will be displayed on the select card in UI.| +|iconClass|Setting for icon to be displayed to the user. EUI supports any known EUI icon, SVG URL, or a lazy loaded React component, ReactElement.| |selectMessage|Short description of action type responsibility, that will be displayed on the select card in UI.| |validateConnector|Validation function for action connector.| |validateParams|Validation function for action params.| @@ -1157,7 +1157,7 @@ Below is a list of steps that should be done to build and register a new action 1. At any suitable place in Kibana, create a file, which will expose an object implementing interface [ActionTypeModel]: ``` -import React, { Fragment, lazy } from 'react'; +import React, { lazy } from 'react'; import { i18n } from '@kbn/i18n'; import { ActionTypeModel, @@ -1230,7 +1230,7 @@ export function getActionType(): ActionTypeModel { 2. Define `actionConnectorFields` as `React.FunctionComponent` - this is the form for action connector. ``` -import React, { Fragment } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFieldText } from '@elastic/eui'; import { EuiTextArea } from '@elastic/eui'; @@ -1252,7 +1252,7 @@ const ExampleConnectorFields: React.FunctionComponent> = ({ action, editActionConfig, errors }) => { const { someConnectorField } = action.config; return ( - + <> 0 && someConnectorField !== undefined} @@ -1267,7 +1267,7 @@ const ExampleConnectorFields: React.FunctionComponent - + ); }; @@ -1277,7 +1277,7 @@ export {ExampleConnectorFields as default}; 3. Define action type params fields using the property of `ActionTypeModel` `actionParamsFields`: ``` -import React, { Fragment } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFieldText } from '@elastic/eui'; import { EuiTextArea } from '@elastic/eui'; @@ -1300,7 +1300,7 @@ const ExampleParamsFields: React.FunctionComponent { const { message } = actionParams; return ( - + <> 0 && message !== undefined} @@ -1315,7 +1315,7 @@ const ExampleParamsFields: React.FunctionComponent - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts b/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts index c1d7a3281b410e..3c5c1b551028ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/action_type_registry.mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import React, { lazy, Fragment } from 'react'; +import React, { lazy } from 'react'; import uuid from 'uuid'; import { ActionTypeModel, ActionTypeRegistryContract } from '../types'; @@ -21,7 +21,7 @@ const createActionTypeRegistryMock = () => { const mockedActionParamsFields = lazy(async () => ({ default() { - return React.createElement(Fragment); + return React.createElement(React.Fragment); }, })); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 2e0614e7ea9637..5acb99c684a961 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -26,7 +26,7 @@ import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common import { setSavedObjectsClient } from '../common/lib/data_apis'; import { KibanaContextProvider } from '../common/lib/kibana'; -const TriggersActionsUIHome = lazy(async () => import('./home')); +const TriggersActionsUIHome = lazy(() => import('./home')); const AlertDetailsRoute = lazy( () => import('./sections/alert_details/components/alert_details_route') ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 57b251fba0d454..2f220855f94743 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiPopover, @@ -62,7 +62,7 @@ export const AddMessageVariables: React.FunctionComponent = ({ ); if ((messageVariables?.length ?? 0) === 0) { - return ; + return <>; } return ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx index 2c25220598b03e..e2d6237af85da2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiComboBox, EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -46,7 +46,7 @@ export const EmailParamsFields = ({ }, [defaultMessage]); return ( - + <> + <> {!addCC ? ( setAddCC(true)}> @@ -77,7 +77,7 @@ export const EmailParamsFields = ({ ) : null} - + } > - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index ba6a5fa2079dca..ff7fd026f8e31b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import logo from './logo.svg'; import { JiraActionConnector, JiraConfig, JiraSecrets, JiraActionParams } from './types'; import * as i18n from './translations'; import { isValidUrl } from '../../../lib/value_validators'; @@ -63,7 +62,7 @@ const validateConnector = ( export function getActionType(): ActionTypeModel { return { id: '.jira', - iconClass: logo, + iconClass: lazy(() => import('./logo')), selectMessage: i18n.JIRA_DESC, actionTypeTitle: i18n.JIRA_TITLE, validateConnector, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index cb2d637972cb82..11123a81440bba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -190,7 +190,7 @@ const JiraParamsFields: React.FunctionComponent + <> <> - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg deleted file mode 100644 index 8560cf7e270c89..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.tsx new file mode 100644 index 00000000000000..2e8f1d5ef3bd70 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.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'; + +const Logo = () => ( + + + + + + + + + +); + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/logo.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/logo.tsx new file mode 100644 index 00000000000000..20db34351c6b12 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/logo.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +const Logo = () => ( + + + + + + +); + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg deleted file mode 100644 index b11dcb3570c266..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx index 7a60d79d331379..eae8690dbdd988 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.test.tsx @@ -25,7 +25,7 @@ beforeAll(() => { describe('actionTypeRegistry.get() works', () => { test('action type static data is as expected', () => { expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); - expect(actionTypeModel.iconClass).toEqual('test-file-stub'); + expect(actionTypeModel.actionTypeTitle).toEqual('Send to PagerDuty'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx index cae4221e5d7ce9..310c5cae24566c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty.tsx @@ -20,7 +20,6 @@ import { PagerDutyActionParams, EventActionOptions, } from '.././types'; -import pagerDutySvg from './pagerduty.svg'; import { hasMustacheTokens } from '../../../lib/has_mustache_tokens'; export function getActionType(): ActionTypeModel< @@ -30,7 +29,7 @@ export function getActionType(): ActionTypeModel< > { return { id: '.pagerduty', - iconClass: pagerDutySvg, + iconClass: lazy(() => import('./logo')), selectMessage: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.pagerDutyAction.selectMessageText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx index 8c9f809b974477..7e9a5770c2158e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -21,7 +21,7 @@ const PagerDutyActionConnectorFields: React.FunctionComponent< const { apiUrl } = action.config; const { routingKey } = action.secrets; return ( - + <> - + <> {getEncryptedFieldNotifyLabel( !action.id, 1, @@ -94,9 +94,9 @@ const PagerDutyActionConnectorFields: React.FunctionComponent< } }} /> - + - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx index 98dd9c6bf84315..4961a27fd0ac15 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { isUndefined } from 'lodash'; @@ -102,7 +102,7 @@ const PagerDutyParamsFields: React.FunctionComponent + <> ) : null} - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.tsx similarity index 77% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.tsx index 553c2c62b7191c..325893756e2f42 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.svg +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.tsx @@ -1,3 +1,20 @@ - - - +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +const Logo = () => ( + + + +); + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index a8fe5e8ae4b6ae..e7074b7506e7a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import logo from './logo.svg'; import { ResilientActionConnector, ResilientConfig, @@ -72,7 +71,7 @@ export function getActionType(): ActionTypeModel< > { return { id: '.resilient', - iconClass: logo, + iconClass: lazy(() => import('./logo')), selectMessage: i18n.DESC, actionTypeTitle: i18n.TITLE, validateConnector, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx index 8444f5a2c5ca91..4642226d402221 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { EuiFormRow, EuiComboBox, @@ -165,7 +165,7 @@ const ResilientParamsFields: React.FunctionComponent + <>

    Incident

    @@ -251,7 +251,7 @@ const ResilientParamsFields: React.FunctionComponent - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx index 2a6d21f31973b9..6397ce7bc184ea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/server_log/server_log_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiSelect, EuiFormRow } from '@elastic/eui'; import { ActionParamsProps } from '../../../../types'; @@ -48,7 +48,7 @@ export const ServerLogParamsFields: React.FunctionComponent< }, [defaultMessage]); return ( - + <> - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.svg deleted file mode 100644 index dcd022a8dca18d..00000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.tsx new file mode 100644 index 00000000000000..f7f79d387c62c4 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/logo.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +function Logo() { + return ( + + + + + + ); +} + +// eslint-disable-next-line import/no-default-export +export default Logo; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index b1664656c0d14d..a6cc116d3d7b4b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import logo from './logo.svg'; import { ServiceNowActionConnector, ServiceNowConfig, @@ -68,7 +67,7 @@ export function getServiceNowITSMActionType(): ActionTypeModel< > { return { id: '.servicenow', - iconClass: logo, + iconClass: lazy(() => import('./logo')), selectMessage: i18n.SERVICENOW_ITSM_DESC, actionTypeTitle: i18n.SERVICENOW_ITSM_TITLE, validateConnector, @@ -103,7 +102,7 @@ export function getServiceNowSIRActionType(): ActionTypeModel< > { return { id: '.servicenow-sir', - iconClass: logo, + iconClass: lazy(() => import('./logo')), selectMessage: i18n.SERVICENOW_SIR_DESC, actionTypeTitle: i18n.SERVICENOW_SIR_TITLE, validateConnector, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index 84326a7ae9be8b..dbd6fec3dad190 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiFormRow, EuiSelect, @@ -146,7 +146,7 @@ const ServiceNowParamsFields: React.FunctionComponent< }, [actionParams]); return ( - + <>

    {i18n.INCIDENT}

    @@ -270,7 +270,7 @@ const ServiceNowParamsFields: React.FunctionComponent< inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined} label={i18n.COMMENTS_LABEL} /> -
    + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx index 95a17c205801c4..be6756b1c1049d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { EuiFormRow, EuiSelect, @@ -142,7 +142,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< }, [actionParams]); return ( - + <>

    {i18n.INCIDENT}

    @@ -276,7 +276,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined} label={i18n.COMMENTS_LABEL} /> -
    + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx index 677eb8d7d05f95..ce6cda1294adc7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiFieldText, EuiFormRow, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -21,7 +21,7 @@ const SlackActionFields: React.FunctionComponent< const { webhookUrl } = action.secrets; return ( - + <> - + <> {getEncryptedFieldNotifyLabel( !action.id, 1, @@ -68,9 +68,9 @@ const SlackActionFields: React.FunctionComponent< } }} /> - + - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/logo.tsx similarity index 91% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/logo.tsx index ab07be8f1ef0a8..42b1fa2c2a0da3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.svg +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/logo.tsx @@ -1,7 +1,32 @@ - - - + - +MS0xMlQxOTo1Nzo0NSswMzowMIKUWWYAAAAASUVORK5CYII=" + /> + +); + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx index 00d860fc54110b..e8c7be7311c1ce 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams.tsx @@ -7,7 +7,6 @@ import { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import teamsSvg from './teams.svg'; import { ActionTypeModel, GenericValidationResult, @@ -19,7 +18,7 @@ import { isValidUrl } from '../../../lib/value_validators'; export function getActionType(): ActionTypeModel { return { id: '.teams', - iconClass: teamsSvg, + iconClass: lazy(() => import('./logo')), selectMessage: i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.teamsAction.selectMessageText', { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx index 9a93d29cfcb155..d3231f52b4d7bf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -110,7 +110,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< let headerControl; if (hasHeaders) { headerControl = ( - + <>
    - + ); } @@ -220,7 +220,7 @@ const WebhookActionConnectorFields: React.FunctionComponent< }); return ( - + <>
  • - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx index d75ab102a8e0ca..762526dfd7fa78 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/health_check.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { Option, none, some, fold } from 'fp-ts/lib/Option'; import { pipe } from 'fp-ts/lib/pipeable'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -69,16 +69,16 @@ export const HealthCheck: React.FunctionComponent = ({ fold( () => waitForCheck ? ( - + <> - + ) : ( - {children} + <>{children} ), (healthCheck) => { return healthCheck?.isSufficientlySecure && healthCheck?.hasPermanentEncryptionKey ? ( - {children} + <>{children} ) : !healthCheck.isAlertsAvailable ? ( ) : !healthCheck.isSufficientlySecure && !healthCheck.hasPermanentEncryptionKey ? ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx index e56fad409d98f4..84ac46605905e5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/prompts/empty_connectors_prompt.tsx @@ -6,7 +6,7 @@ */ import { FormattedMessage } from '@kbn/i18n/react'; -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiButton, EuiEmptyPrompt, EuiIcon, EuiSpacer, EuiTitle } from '@elastic/eui'; import './empty_connectors_prompt.scss'; @@ -14,7 +14,7 @@ export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => vo + <> @@ -27,7 +27,7 @@ export const EmptyConnectorsPrompt = ({ onCTAClicked }: { onCTAClicked: () => vo /> - + } body={

    diff --git a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx index b77593c9905509..20aec6974d395b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/home.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/home.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { lazy, useEffect } from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -26,11 +26,15 @@ import { getAlertingSectionBreadcrumb } from './lib/breadcrumb'; import { getCurrentDocTitle } from './lib/doc_title'; import { hasShowActionsCapability } from './lib/capabilities'; -import { ActionsConnectorsList } from './sections/actions_connectors_list/components/actions_connectors_list'; -import { AlertsList } from './sections/alerts_list/components/alerts_list'; import { HealthCheck } from './components/health_check'; import { HealthContextProvider } from './context/health_context'; import { useKibana } from '../common/lib/kibana'; +import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; + +const ActionsConnectorsList = lazy( + () => import('./sections/actions_connectors_list/components/actions_connectors_list') +); +const AlertsList = lazy(() => import('./sections/alerts_list/components/alerts_list')); export interface MatchParams { section: Section; @@ -137,32 +141,24 @@ export const TriggersActionsUIHome: React.FunctionComponent - - {canShowActions && ( - ( - - - - - + + + + {canShowActions && ( + )} - /> - )} - ( - - - - - - )} - /> - + + + + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx index f9855cc9d71308..6b115abc590ccc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.test.tsx @@ -61,7 +61,7 @@ describe('checkActionTypeEnabled', () => { > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx index e4e67002298ee7..1c2a56f4cccaad 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/check_action_type_enabled.tsx @@ -39,7 +39,7 @@ const getLicenseCheckResult = (actionType: ActionType) => { { // The "re-enable" terminology is used here because this message is used when an alert // action was previously enabled and needs action to be re-enabled. description={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.actionTypeDisabledByLicenseMessageDescription', + 'xpack.triggersActionsUI.licenseCheck.actionTypeDisabledByLicenseMessageDescription', { defaultMessage: 'To re-enable this action, please upgrade your license.' } )} className="actCheckActionTypeEnabled__disabledActionWarningCard" @@ -58,7 +58,7 @@ const getLicenseCheckResult = (actionType: ActionType) => { } @@ -76,7 +76,7 @@ const configurationCheckResult = { messageCard: ( + <> - + ); const FieldsComponent = actionTypeRegistered.actionConnectorFields; 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 174407e7edec50..ad727be58280f4 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, lazy } from 'react'; +import React, { lazy } from 'react'; import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { EuiAccordion } from '@elastic/eui'; import { coreMock } from '../../../../../../../src/core/public/mocks'; @@ -34,7 +34,7 @@ const setHasActionsWithBrokenConnector = jest.fn(); describe('action_form', () => { const mockedActionParamsFields = lazy(async () => ({ default() { - return ; + return <>; }, })); @@ -45,7 +45,7 @@ describe('action_form', () => { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: () => <>, requiresAppContext: false, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 55ebbbc6f3edd1..e9f79633ef5207 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -30,7 +30,6 @@ import { ActionTypeRegistryContract, } from '../../../types'; import { SectionLoading } from '../../components/section_loading'; -import { ConnectorAddModal } from './connector_add_modal'; import { ActionTypeForm, ActionTypeFormProps } from './action_type_form'; import { AddConnectorInline } from './connector_add_inline'; import { actionTypeCompare } from '../../lib/action_type_compare'; @@ -43,6 +42,8 @@ import { import { ActionGroup, AlertActionParam } from '../../../../../alerting/common'; import { useKibana } from '../../../common/lib/kibana'; import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params'; +import { ConnectorAddModal } from '.'; +import { suspendedComponentWithProps } from '../../lib/suspended_component_with_props'; export interface ActionGroupWithMessageVariables extends ActionGroup { omitOptionalMessageVariables?: boolean; @@ -124,7 +125,7 @@ export const ActionForm = ({ } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadConnectorTypesMessage', + 'xpack.triggersActionsUI.sections.actionForm.unableToLoadConnectorTypesMessage', { defaultMessage: 'Unable to load connector types' } ), }); @@ -145,7 +146,7 @@ export const ActionForm = ({ } catch (e) { toasts.addDanger({ title: i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.unableToLoadActionsMessage', + 'xpack.triggersActionsUI.sections.actionForm.unableToLoadActionsMessage', { defaultMessage: 'Unable to load connectors', } @@ -193,7 +194,7 @@ export const ActionForm = ({ function addActionType(actionTypeModel: ActionTypeModel) { if (!defaultActionGroupId) { toasts!.addDanger({ - title: i18n.translate('xpack.triggersActionsUI.sections.alertForm.unableToAddAction', { + title: i18n.translate('xpack.triggersActionsUI.sections.actionForm.unableToAddAction', { defaultMessage: 'Unable to add action, because default action group is not defined', }), }); @@ -271,7 +272,14 @@ export const ActionForm = ({ label={actionTypesIndex[item.id].name} onClick={() => addActionType(item)} > - + ); @@ -291,17 +299,17 @@ export const ActionForm = ({ return isLoadingConnectors ? ( ) : ( - + <>

    @@ -354,54 +362,56 @@ export const ActionForm = ({ ?.validateParams(actionItem.params); return ( - { - setActiveActionItem({ actionTypeId: actionItem.actionTypeId, indices: [index] }); - setAddModalVisibility(true); - }} - onConnectorSelected={(id: string) => { - setActionIdByIndex(id, index); - }} - actionTypeRegistry={actionTypeRegistry} - onDeleteAction={() => { - const updatedActions = actions.filter( - (_item: AlertAction, i: number) => i !== index - ); - setActions(updatedActions); - setIsAddActionPanelOpen( - updatedActions.filter((item: AlertAction) => item.id !== actionItem.id).length === - 0 - ); - setActiveActionItem(undefined); - }} - /> + + { + setActiveActionItem({ actionTypeId: actionItem.actionTypeId, indices: [index] }); + setAddModalVisibility(true); + }} + onConnectorSelected={(id: string) => { + setActionIdByIndex(id, index); + }} + actionTypeRegistry={actionTypeRegistry} + onDeleteAction={() => { + const updatedActions = actions.filter( + (_item: AlertAction, i: number) => i !== index + ); + setActions(updatedActions); + setIsAddActionPanelOpen( + updatedActions.filter((item: AlertAction) => item.id !== actionItem.id) + .length === 0 + ); + setActiveActionItem(undefined); + }} + /> + + ); })} {isAddActionPanelOpen ? ( - + <>
    @@ -431,7 +441,7 @@ export const ActionForm = ({ {isLoadingActionTypes ? ( @@ -439,7 +449,7 @@ export const ActionForm = ({ actionTypeNodes )}
    -
    + ) : ( @@ -449,7 +459,7 @@ export const ActionForm = ({ onClick={() => setIsAddActionPanelOpen(true)} > @@ -468,7 +478,7 @@ export const ActionForm = ({ actionTypeRegistry={actionTypeRegistry} /> ) : null} - + ); }; 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 48c6c1b42d7af7..2690aeaffad324 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 @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, Suspense, useEffect, useState } from 'react'; +import React, { Suspense, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -166,7 +166,7 @@ export const ActionTypeForm = ({ isActionGroupDisabledForActionType ? isActionGroupDisabledForActionType(actionGroupId, actionTypeId) ? i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.addNewActionConnectorActionGroup.display', + 'xpack.triggersActionsUI.sections.actionTypeForm.addNewActionConnectorActionGroup.display', { defaultMessage: '{actionGroupName} (Not Currently Supported)', values: { actionGroupName }, @@ -202,9 +202,9 @@ export const ActionTypeForm = ({ ); const accordionContent = checkEnabledResult.isEnabled ? ( - + <> {actionGroups && selectedActionGroup && setActionGroupIdByIndex && ( - + <> @@ -240,7 +240,7 @@ export const ActionTypeForm = ({ - + )} @@ -248,7 +248,7 @@ export const ActionTypeForm = ({ fullWidth label={ ) : null @@ -305,88 +305,86 @@ export const ActionTypeForm = ({ ) : null} - + ) : ( checkEnabledResult.messageCard ); return ( - - - - - - - -
    - + + + + + + +
    + + + + + {selectedActionGroup && !isOpen && ( - + {selectedActionGroup.name} - {selectedActionGroup && !isOpen && ( - - {selectedActionGroup.name} - + )} + + {checkEnabledResult.isEnabled === false && ( + <> + + )} - - {checkEnabledResult.isEnabled === false && ( - - - - )} - - -
    -
    -
    -
    - } - extraAction={ - - } - > - {accordionContent} - - - + + +
    +
    +
    +
    + } + extraAction={ + + } + > + {accordionContent} + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 6dc75b318a8f07..d3a6d662720ca9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useState, Fragment, useReducer } from 'react'; +import React, { useCallback, useState, useReducer } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -29,8 +29,8 @@ import { ActionConnectorForm, getConnectorErrors } from './action_connector_form import { ActionType, ActionConnector, - ActionTypeRegistryContract, UserConfiguredActionConnector, + ConnectorAddFlyoutProps, } from '../../../types'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; @@ -39,15 +39,6 @@ import { useKibana } from '../../../common/lib/kibana'; import { createConnectorReducer, InitialConnector, ConnectorReducer } from './connector_reducer'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; -export interface ConnectorAddFlyoutProps { - onClose: () => void; - actionTypes?: ActionType[]; - onTestConnector?: (connector: ActionConnector) => void; - reloadConnectors?: () => Promise; - consumer?: string; - actionTypeRegistry: ActionTypeRegistryContract; -} - const ConnectorAddFlyout: React.FunctionComponent = ({ onClose, actionTypes, @@ -199,7 +190,7 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ }; saveButton = ( - + <> {onTestConnector && ( = ({ /> - + ); } @@ -251,7 +242,7 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ ) : null} {actionTypeModel && actionType ? ( - + <>

    = ({ {actionTypeModel.selectMessage} - + ) : (

    @@ -285,7 +276,7 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ !actionType && hasActionsUpgradeableByTrial ? ( ) : ( - + <> ) } > diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx index 9a9583313bcdb7..0cdcf8bd44413b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_inline.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -68,7 +68,7 @@ export const AddConnectorInline = ({ const noConnectorsLabel = ( @@ -117,7 +117,7 @@ export const AddConnectorInline = ({ fullWidth label={ } @@ -162,8 +162,9 @@ export const AddConnectorInline = ({ ); return ( - + <> } /> @@ -211,7 +212,7 @@ export const AddConnectorInline = ({ color="danger" className="actAccordionActionForm__extraAction" aria-label={i18n.translate( - 'xpack.triggersActionsUI.sections.alertForm.accordion.deleteIconAriaLabel', + 'xpack.triggersActionsUI.sections.connectorAddInline.accordion.deleteIconAriaLabel', { defaultMessage: 'Delete', } @@ -236,7 +237,7 @@ export const AddConnectorInline = ({ onClick={onAddConnector} > @@ -247,7 +248,7 @@ export const AddConnectorInline = ({

    @@ -255,6 +256,9 @@ export const AddConnectorInline = ({ )}
    -
    + ); }; + +// eslint-disable-next-line import/no-default-export +export { AddConnectorInline as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx index ccff4f5853b1b6..c18f6955d12177 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.test.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; -import { ConnectorAddModal } from './connector_add_modal'; +import ConnectorAddModal from './connector_add_modal'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { ActionType, ConnectorValidationResult, GenericValidationResult } from '../../../types'; import { useKibana } from '../../../common/lib/kibana'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 8732727b9a77a8..d01ee08df23946 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -35,15 +35,16 @@ import { import { useKibana } from '../../../common/lib/kibana'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; -interface ConnectorAddModalProps { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +type ConnectorAddModalProps = { actionType: ActionType; onClose: () => void; postSaveEventHandler?: (savedAction: ActionConnector) => void; consumer?: string; actionTypeRegistry: ActionTypeRegistryContract; -} +}; -export const ConnectorAddModal = ({ +const ConnectorAddModal = ({ actionType, onClose, postSaveEventHandler, @@ -216,3 +217,6 @@ export const ConnectorAddModal = ({ ); }; + +// eslint-disable-next-line import/no-default-export +export { ConnectorAddModal as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 6c08b0b0b1ac53..66a4dcc452c518 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useReducer, useState, Fragment } from 'react'; +import React, { useCallback, useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -30,7 +30,8 @@ import { ActionConnectorForm, getConnectorErrors } from './action_connector_form import { TestConnectorForm } from './test_connector_form'; import { ActionConnector, - ActionTypeRegistryContract, + ConnectorEditFlyoutProps, + EditConectorTabs, UserConfiguredActionConnector, } from '../../../types'; import { ConnectorReducer, createConnectorReducer } from './connector_reducer'; @@ -44,21 +45,7 @@ import './connector_edit_flyout.scss'; import { useKibana } from '../../../common/lib/kibana'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; -export interface ConnectorEditFlyoutProps { - initialConnector: ActionConnector; - onClose: () => void; - tab?: EditConectorTabs; - reloadConnectors?: () => Promise; - consumer?: string; - actionTypeRegistry: ActionTypeRegistryContract; -} - -export enum EditConectorTabs { - Configuration = 'configuration', - Test = 'test', -} - -export const ConnectorEditFlyout = ({ +const ConnectorEditFlyout = ({ initialConnector, onClose, tab = EditConectorTabs.Configuration, @@ -173,7 +160,7 @@ export const ConnectorEditFlyout = ({ }); const flyoutTitle = connector.isPreconfigured ? ( - + <>

    - + ) : (

    @@ -313,7 +300,7 @@ export const ConnectorEditFlyout = ({ consumer={consumer} /> ) : ( - + <> {i18n.translate( 'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', @@ -328,7 +315,7 @@ export const ConnectorEditFlyout = ({ defaultMessage="Learn more about preconfigured connectors." /> - + ) ) : ( {canSave && actionTypeModel && !connector.isPreconfigured ? ( - + <> - + ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts index 6ff8b5ae1d5002..75d29fd4b0c094 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/index.ts @@ -15,3 +15,11 @@ export const ConnectorEditFlyout = suspendedComponentWithProps( lazy(() => import('./connector_edit_flyout')) ); export const ActionForm = suspendedComponentWithProps(lazy(() => import('./action_form'))); + +export const ConnectorAddModal = suspendedComponentWithProps( + lazy(() => import('./connector_add_modal')) +); + +export const AddConnectorInline = suspendedComponentWithProps( + lazy(() => import('./connector_add_inline')) +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx index 8afa2d2b575299..92a17a2e4cfae8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, Suspense } from 'react'; +import React, { Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -102,9 +102,9 @@ export const TestConnectorForm = ({ defaultMessage: 'Run the test', }), children: ( - + <> {executeEnabled ? null : ( - + <>

    - + )} - + ), }, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx index 9102e73690cacf..7b6453e705ec38 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.test.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { mountWithIntl, nextTick } from '@kbn/test/jest'; -import { ActionsConnectorsList } from './actions_connectors_list'; +import ActionsConnectorsList from './actions_connectors_list'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { ReactWrapper } from 'enzyme'; import { act } from 'react-dom/test-utils'; @@ -154,7 +154,7 @@ describe('actions_connectors_list component with items', () => { const mockedActionParamsFields = React.lazy(async () => ({ default() { - return ; + return <>; }, })); @@ -260,7 +260,8 @@ describe('actions_connectors_list component with items', () => { await setup(); await wrapper.find('[data-test-subj="edit1"]').first().simulate('click'); - expect(wrapper.find('ConnectorEditFlyout')).toHaveLength(1); + const edit = await wrapper.find('ConnectorEditFlyout'); + expect(edit).toHaveLength(1); }); }); 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 a322460cde4448..c237bbda486584 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 @@ -25,10 +25,6 @@ import { i18n } from '@kbn/i18n'; import { omit } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { loadAllActions, loadActionTypes, deleteActions } from '../../../lib/action_connector_api'; -import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; -import ConnectorEditFlyout, { - EditConectorTabs, -} from '../../action_connector_form/connector_edit_flyout'; import { hasDeleteActionsCapability, hasSaveActionsCapability, @@ -37,13 +33,20 @@ import { import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation'; import { checkActionTypeEnabled } from '../../../lib/check_action_type_enabled'; import './actions_connectors_list.scss'; -import { ActionConnector, ActionConnectorTableItem, ActionTypeIndex } from '../../../../types'; +import { + ActionConnector, + ActionConnectorTableItem, + ActionTypeIndex, + EditConectorTabs, +} from '../../../../types'; import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; import { useKibana } from '../../../../common/lib/kibana'; import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; +import ConnectorEditFlyout from '../../action_connector_form/connector_edit_flyout'; +import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; -export const ActionsConnectorsList: React.FunctionComponent = () => { +const ActionsConnectorsList: React.FunctionComponent = () => { const { http, notifications: { toasts }, @@ -440,6 +443,9 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { ); }; +// eslint-disable-next-line import/no-default-export +export { ActionsConnectorsList as default }; + function getActionsCountByActionType(actions: ActionConnector[], actionTypeId: string) { return actions.filter((action) => action.actionTypeId === actionTypeId).length; } @@ -455,7 +461,7 @@ const DeleteOperation: React.FunctionComponent<{ = ({ {hasEditButton ? ( - + <> {' '} = ({ onSave={setAlert} /> )} - + ) : null} @@ -201,7 +201,7 @@ export const AlertDetails: React.FunctionComponent = ({ {uniqueActions && uniqueActions.length ? ( - + <>

    = ({ ))} - + ) : null} @@ -336,7 +336,7 @@ export const AlertDetails: React.FunctionComponent = ({ readOnly={!canSaveAlert} /> ) : ( - + <> = ({ />

    -
    + )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx index 5ba4c466f6fadf..29290af0d02852 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_instances.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import moment, { Duration } from 'moment'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiHealth, EuiSpacer, EuiSwitch, EuiToolTip } from '@elastic/eui'; @@ -112,7 +112,7 @@ export const alertInstancesTableColumns = ( ), render: (alertInstance: AlertInstanceListItem) => { return ( - + <> onMuteAction(alertInstance)} /> - + ); }, sortable: false, @@ -167,7 +167,7 @@ export function AlertInstances({ }; return ( - + <> - + ); } export const AlertInstancesWithApi = withBulkAlertOperations(AlertInstances); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index a2463d785a3ebf..cb43c168aa999d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -12,11 +12,12 @@ import { act } from 'react-dom/test-utils'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormLabel } from '@elastic/eui'; import { coreMock } from '../../../../../../../src/core/public/mocks'; -import AlertAdd, { AlertAddProps } from './alert_add'; +import AlertAdd from './alert_add'; import { createAlert } from '../../lib/alert_api'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; import { Alert, + AlertAddProps, AlertFlyoutCloseReason, ConnectorValidationResult, GenericValidationResult, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index bcae77f896b71e..a40f77998d6ee4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -11,12 +11,11 @@ import { EuiTitle, EuiFlyoutHeader, EuiFlyout, EuiFlyoutBody, EuiPortal } from ' import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import { - ActionTypeRegistryContract, Alert, - AlertTypeRegistryContract, AlertTypeParams, AlertUpdates, AlertFlyoutCloseReason, + AlertAddProps, } from '../../../types'; import { AlertForm, getAlertErrors, isValidAlert } from './alert_form'; import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer'; @@ -31,20 +30,6 @@ import { useKibana } from '../../../common/lib/kibana'; import { hasAlertChanged, haveAlertParamsChanged } from './has_alert_changed'; import { getAlertWithInvalidatedFields } from '../../lib/value_validators'; -export interface AlertAddProps> { - consumer: string; - alertTypeRegistry: AlertTypeRegistryContract; - actionTypeRegistry: ActionTypeRegistryContract; - onClose: (reason: AlertFlyoutCloseReason) => void; - alertTypeId?: string; - canChangeTrigger?: boolean; - initialValues?: Partial; - /** @deprecated use `onSave` as a callback after an alert is saved*/ - reloadAlerts?: () => Promise; - onSave?: () => Promise; - metadata?: MetaData; -} - const AlertAdd = ({ consumer, alertTypeRegistry, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx index 6c2f5aecfcb7c7..dd0a7df38eb629 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_conditions_group.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, PropsWithChildren } from 'react'; +import React, { PropsWithChildren } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiButtonIcon, EuiTitle } from '@elastic/eui'; import { AlertConditionsProps, ActionGroupWithCondition } from './alert_conditions'; @@ -55,7 +55,7 @@ export const AlertConditionsGroup = ({ ...otherProps, }) ) : ( - + <> )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 0736e28f193b81..49dd92b67ee41c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -98,7 +98,7 @@ describe('alert_edit', () => { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: () => <>, requiresAppContext: false, }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index d704111858e4fd..f6569f32088eec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useReducer, useState } from 'react'; +import React, { useReducer, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -23,12 +23,7 @@ import { } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ActionTypeRegistryContract, - Alert, - AlertFlyoutCloseReason, - AlertTypeRegistryContract, -} from '../../../types'; +import { Alert, AlertEditProps, AlertFlyoutCloseReason } from '../../../types'; import { AlertForm, getAlertErrors, isValidAlert } from './alert_form'; import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; import { updateAlert } from '../../lib/alert_api'; @@ -39,17 +34,6 @@ import { ConfirmAlertClose } from './confirm_alert_close'; import { hasAlertChanged } from './has_alert_changed'; import { getAlertWithInvalidatedFields } from '../../lib/value_validators'; -export interface AlertEditProps> { - initialAlert: Alert; - alertTypeRegistry: AlertTypeRegistryContract; - actionTypeRegistry: ActionTypeRegistryContract; - onClose: (reason: AlertFlyoutCloseReason) => void; - /** @deprecated use `onSave` as a callback after an alert is saved*/ - reloadAlerts?: () => Promise; - onSave?: () => Promise; - metadata?: MetaData; -} - export const AlertEdit = ({ initialAlert, onClose, @@ -149,7 +133,7 @@ export const AlertEdit = ({ {hasActionsDisabled && ( - + <> - + )} { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: () => <>, requiresAppContext: false, }; @@ -72,7 +72,7 @@ describe('alert_form', () => { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: () => <>, requiresAppContext: true, }; @@ -84,7 +84,7 @@ describe('alert_form', () => { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: () => <>, requiresAppContext: false, }; @@ -322,7 +322,7 @@ describe('alert_form', () => { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: () => <>, requiresAppContext: true, }, { @@ -333,7 +333,7 @@ describe('alert_form', () => { validate: (): ValidationResult => { return { errors: {} }; }, - alertParamsExpression: () => , + alertParamsExpression: () => <>, requiresAppContext: false, }, ]); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 6bb485bc7fbb84..b4b6477fd59470 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -470,35 +470,34 @@ export const AlertForm = ({ ); return ( - - - {alertTypeListItemHtml} - - ) + + {alertTypeListItemHtml} + + ) + } + isDisabled={!item.checkEnabledResult.isEnabled} + onClick={() => { + setAlertProperty('alertTypeId', item.id); + setActions([]); + setAlertTypeModel(item.alertTypeItem); + setAlertProperty('params', {}); + if (alertTypesIndex && alertTypesIndex.has(item.id)) { + setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); } - isDisabled={!item.checkEnabledResult.isEnabled} - onClick={() => { - setAlertProperty('alertTypeId', item.id); - setActions([]); - setAlertTypeModel(item.alertTypeItem); - setAlertProperty('params', {}); - if (alertTypesIndex && alertTypesIndex.has(item.id)) { - setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); - } - }} - /> - + }} + /> ); })} @@ -507,7 +506,7 @@ export const AlertForm = ({ )); const alertTypeDetails = ( - + <> @@ -605,11 +604,11 @@ export const AlertForm = ({ selectedAlertType ? ( <> {errors.actionConnectors.length >= 1 ? ( - + <> - + ) : null} ) : null} - + ); const labelForAlertChecked = ( @@ -793,9 +792,9 @@ export const AlertForm = ({ {alertTypeModel ? ( - {alertTypeDetails} + <>{alertTypeDetails} ) : availableAlertTypes.length ? ( - + <> {errors.alertTypeId.length >= 1 && alert.alertTypeId !== undefined ? ( - + <> - + ) : null} {alertTypeNodes} - + ) : alertTypesIndex ? ( ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx index b774fd702fadc4..15b228467cf2d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_notify_when.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment, useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -39,7 +39,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ ), 'data-test-subj': 'onActionGroupChange', dropdownDisplay: ( - + <> > = [ />

    -
    + ), }, { @@ -67,7 +67,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ ), 'data-test-subj': 'onActiveAlert', dropdownDisplay: ( - + <> > = [ />

    -
    + ), }, { @@ -95,7 +95,7 @@ const NOTIFY_WHEN_OPTIONS: Array> = [ ), 'data-test-subj': 'onThrottleInterval', dropdownDisplay: ( - + <> > = [ />

    -
    + ), }, ]; @@ -173,7 +173,7 @@ export const AlertNotifyWhen = ({ ); return ( - + <> @@ -184,7 +184,7 @@ export const AlertNotifyWhen = ({ onChange={onNotifyWhenValueChange} /> {showCustomThrottleOpts && ( - + <> @@ -227,11 +227,11 @@ export const AlertNotifyWhen = ({ - + )} -
    + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index d43dd9f05344fe..1fb688c4dd6bf4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { capitalize, sortBy } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useEffect, useState, Fragment } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiBasicTable, EuiBadge, @@ -479,7 +479,7 @@ export const AlertsList: React.FunctionComponent = () => { : false; const table = ( - + <> {selectedIds.length > 0 && authorizedToModifySelectedAlerts && ( @@ -713,7 +713,7 @@ export const AlertsList: React.FunctionComponent = () => { onCancel={() => setManageLicenseModalOpts(null)} /> )} - + ); const loadedItems = convertAlertsToTableItems( @@ -782,6 +782,9 @@ export const AlertsList: React.FunctionComponent = () => { ); }; +// eslint-disable-next-line import/no-default-export +export { AlertsList as default }; + const noPermissionPrompt = ( { > {children && React.Children.map(children, (child) => - React.isValidElement(child) ? {React.cloneElement(child, {})} : child + React.isValidElement(child) ? <>{React.cloneElement(child, {})} : child )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx index a6ec9d1b39665e..1430c403407719 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/group_by_over.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, Fragment } from 'react'; +import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { @@ -151,7 +151,7 @@ export const GroupByExpression = ({ {groupByTypes[groupBy].sizeRequired ? ( - + <> 0} error={errors.termSize}> - + ) : null}

    diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx index d650162816f2bb..5c44b6f29178b3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/threshold.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useEffect, useState, Fragment } from 'react'; +import React, { Fragment, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiExpression, diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx index 42444f4b54e866..2698f4ee2e428e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_add_alert_flyout.tsx @@ -5,14 +5,10 @@ * 2.0. */ -import React, { lazy, Suspense } from 'react'; -import type { AlertAddProps } from '../application/sections/alert_form/alert_add'; +import React from 'react'; +import { AlertAdd } from '../application/sections/alert_form'; +import type { AlertAddProps } from '../types'; export const getAddAlertFlyoutLazy = (props: AlertAddProps) => { - const AlertAddFlyoutLazy = lazy(() => import('../application/sections/alert_form/alert_add')); - return ( - - - - ); + return ; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx index 2c211572f28509..09261714cf8cf2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_add_connector_flyout.tsx @@ -5,16 +5,10 @@ * 2.0. */ -import React, { lazy, Suspense } from 'react'; -import type { ConnectorAddFlyoutProps } from '../application/sections/action_connector_form/connector_add_flyout'; +import React from 'react'; +import { ConnectorAddFlyout } from '../application/sections/action_connector_form'; +import type { ConnectorAddFlyoutProps } from '../types'; export const getAddConnectorFlyoutLazy = (props: ConnectorAddFlyoutProps) => { - const ConnectorAddFlyoutLazy = lazy( - () => import('../application/sections/action_connector_form/connector_add_flyout') - ); - return ( - - - - ); + return ; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx index 89b17f5bb15960..26cc1159e5afdd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_alert_flyout.tsx @@ -5,14 +5,10 @@ * 2.0. */ -import React, { lazy, Suspense } from 'react'; -import type { AlertEditProps } from '../application/sections/alert_form/alert_edit'; +import React from 'react'; +import { AlertEdit } from '../application/sections/alert_form'; +import type { AlertEditProps } from '../types'; export const getEditAlertFlyoutLazy = (props: AlertEditProps) => { - const AlertEditFlyoutLazy = lazy(() => import('../application/sections/alert_form/alert_edit')); - return ( - - - - ); + return ; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx index 38002cfe14a150..90ecea56856f57 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_edit_connector_flyout.tsx @@ -5,16 +5,10 @@ * 2.0. */ -import React, { lazy, Suspense } from 'react'; -import type { ConnectorEditFlyoutProps } from '../application/sections/action_connector_form/connector_edit_flyout'; +import React from 'react'; +import { ConnectorEditFlyout } from '../application/sections/action_connector_form'; +import type { ConnectorEditFlyoutProps } from '../types'; export const getEditConnectorFlyoutLazy = (props: ConnectorEditFlyoutProps) => { - const ConnectorEditFlyoutLazy = lazy( - () => import('../application/sections/action_connector_form/connector_edit_flyout') - ); - return ( - - - - ); + return ; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/index.ts b/x-pack/plugins/triggers_actions_ui/public/index.ts index b65086cd6f3e7d..134627929e4a02 100644 --- a/x-pack/plugins/triggers_actions_ui/public/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/index.ts @@ -7,14 +7,6 @@ import { Plugin } from './plugin'; -export { AlertAdd } from './application/sections/alert_form'; -export { - AlertEdit, - AlertConditions, - AlertConditionsGroup, - ActionGroupWithCondition, -} from './application/sections'; - export type { AlertAction, Alert, @@ -37,7 +29,10 @@ export { ConnectorEditFlyout, } from './application/sections/action_connector_form'; -export { loadActionTypes } from './application/lib/action_connector_api'; +export type { ActionGroupWithCondition } from './application/sections'; + +export { AlertConditions, AlertConditionsGroup } from './application/sections'; + export * from './common'; export function plugin() { @@ -47,6 +42,8 @@ export function plugin() { export { Plugin }; export * from './plugin'; -export { TIME_UNITS } from './application/constants'; +export { loadActionTypes } from './application/lib/action_connector_api/connector_types'; + +export type { TIME_UNITS } from './application/constants'; export { getTimeUnitLabel } from './common/lib/get_time_unit_label'; export type { TriggersAndActionsUiServices } from '../public/application/app'; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 9666f8ab1b16b5..dfc1cc88e15bcf 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -5,10 +5,6 @@ * 2.0. */ -import type { ConnectorAddFlyoutProps } from './application/sections/action_connector_form/connector_add_flyout'; -import type { ConnectorEditFlyoutProps } from './application/sections/action_connector_form/connector_edit_flyout'; -import type { AlertAddProps } from './application/sections/alert_form/alert_add'; -import type { AlertEditProps } from './application/sections/alert_form/alert_edit'; import type { TriggersAndActionsUIPublicPluginStart } from './plugin'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; @@ -17,7 +13,14 @@ import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; import { TypeRegistry } from './application/type_registry'; -import { ActionTypeModel, AlertTypeModel } from './types'; +import { + ActionTypeModel, + AlertAddProps, + AlertEditProps, + AlertTypeModel, + ConnectorAddFlyoutProps, + ConnectorEditFlyoutProps, +} from './types'; function createStartMock(): TriggersAndActionsUIPublicPluginStart { const actionTypeRegistry = new TypeRegistry(); diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index a027f25d15eb7b..62daf2ad198f36 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -32,11 +32,14 @@ import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; -import type { ActionTypeModel, AlertTypeModel } from './types'; -import type { ConnectorAddFlyoutProps } from './application/sections/action_connector_form/connector_add_flyout'; -import type { ConnectorEditFlyoutProps } from './application/sections/action_connector_form/connector_edit_flyout'; -import type { AlertAddProps } from './application/sections/alert_form/alert_add'; -import type { AlertEditProps } from './application/sections/alert_form/alert_edit'; +import type { + ActionTypeModel, + AlertAddProps, + AlertEditProps, + AlertTypeModel, + ConnectorAddFlyoutProps, + ConnectorEditFlyoutProps, +} from './types'; export interface TriggersAndActionsUIPublicPluginSetup { actionTypeRegistry: TypeRegistry; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 6db5634be2221a..0f2b961b1f2da8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -10,6 +10,7 @@ import type { DocLinksStart } from 'kibana/public'; import { ComponentType } from 'react'; import { ChartsPluginSetup } from 'src/plugins/charts/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; +import { IconType } from '@elastic/eui'; import { ActionType, AlertHistoryEsIndexConnectorId, @@ -103,7 +104,7 @@ export interface Sorting { export interface ActionTypeModel { id: string; - iconClass: string; + iconClass: IconType; selectMessage: string; actionTypeTitle?: string; validateConnector: ( @@ -247,3 +248,50 @@ export interface AlertTypeModel void; + actionTypes?: ActionType[]; + onTestConnector?: (connector: ActionConnector) => void; + reloadConnectors?: () => Promise; + consumer?: string; + actionTypeRegistry: ActionTypeRegistryContract; +} +export enum EditConectorTabs { + Configuration = 'configuration', + Test = 'test', +} + +export interface ConnectorEditFlyoutProps { + initialConnector: ActionConnector; + onClose: () => void; + tab?: EditConectorTabs; + reloadConnectors?: () => Promise; + consumer?: string; + actionTypeRegistry: ActionTypeRegistryContract; +} + +export interface AlertEditProps> { + initialAlert: Alert; + alertTypeRegistry: AlertTypeRegistryContract; + actionTypeRegistry: ActionTypeRegistryContract; + onClose: (reason: AlertFlyoutCloseReason) => void; + /** @deprecated use `onSave` as a callback after an alert is saved*/ + reloadAlerts?: () => Promise; + onSave?: () => Promise; + metadata?: MetaData; +} + +export interface AlertAddProps> { + consumer: string; + alertTypeRegistry: AlertTypeRegistryContract; + actionTypeRegistry: ActionTypeRegistryContract; + onClose: (reason: AlertFlyoutCloseReason) => void; + alertTypeId?: string; + canChangeTrigger?: boolean; + initialValues?: Partial; + /** @deprecated use `onSave` as a callback after an alert is saved*/ + reloadAlerts?: () => Promise; + onSave?: () => Promise; + metadata?: MetaData; +} diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu.tsx index 1d5a375acedee1..6186d6f38b9685 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu.tsx @@ -6,70 +6,12 @@ */ import React from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { - createExploratoryViewUrl, - HeaderMenuPortal, - SeriesUrl, -} from '../../../../../observability/public'; +import { HeaderMenuPortal } from '../../../../../observability/public'; import { AppMountParameters } from '../../../../../../../src/core/public'; -import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; -import { useGetUrlParams } from '../../../hooks'; +import { ActionMenuContent } from './action_menu_content'; -const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { - defaultMessage: 'Add data', -}); - -const ANALYZE_DATA = i18n.translate('xpack.uptime.analyzeDataButtonLabel', { - defaultMessage: 'Analyze data', -}); - -const ANALYZE_MESSAGE = i18n.translate('xpack.uptime.analyzeDataButtonLabel.message', { - defaultMessage: - 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', -}); - -export const ActionMenu = ({ appMountParameters }: { appMountParameters: AppMountParameters }) => { - const kibana = useKibana(); - const { basePath } = useUptimeSettingsContext(); - const { dateRangeStart, dateRangeEnd } = useGetUrlParams(); - - const syntheticExploratoryViewLink = createExploratoryViewUrl( - { - 'synthetics-series': { - dataType: 'synthetics', - time: { from: dateRangeStart, to: dateRangeEnd }, - } as SeriesUrl, - }, - basePath - ); - - return ( - - - - {ANALYZE_MESSAGE}

    }> - - {ANALYZE_DATA} - -
    -
    - - - {ADD_DATA_LABEL} - - -
    -
    - ); -}; +export const ActionMenu = ({ appMountParameters }: { appMountParameters: AppMountParameters }) => ( + + + +); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx new file mode 100644 index 00000000000000..bc5eab6f92111e --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx @@ -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 React from 'react'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { ActionMenuContent } from './action_menu_content'; + +describe('ActionMenuContent', () => { + it('renders alerts dropdown', async () => { + const { getByLabelText, getByText } = render(); + + const alertsDropdown = getByLabelText('Open alert context menu'); + fireEvent.click(alertsDropdown); + + await waitFor(() => { + expect(getByText('Create alert')); + expect(getByText('Manage alerts')); + }); + }); + + it('renders settings link', () => { + const { getByRole, getByText } = render(); + + const settingsAnchor = getByRole('link', { name: 'Navigate to the Uptime settings page' }); + expect(settingsAnchor.getAttribute('href')).toBe('/settings'); + expect(getByText('Settings')); + }); + + it('renders exploratory view link', () => { + const { getByLabelText, getByText } = render(); + + const analyzeAnchor = getByLabelText( + 'Navigate to the "Analyze Data" view to visualize Synthetics/User data' + ); + + expect(analyzeAnchor.getAttribute('href')).toContain('/app/observability/exploratory-view'); + expect(getByText('Analyze data')); + }); + + it('renders Add Data link', () => { + const { getByLabelText, getByText } = render(); + + const addDataAnchor = getByLabelText('Navigate to a tutorial about adding Uptime data'); + + // this href value is mocked, so it doesn't correspond to the real link + // that Kibana core services will provide + expect(addDataAnchor.getAttribute('href')).toBe('/app/uptime'); + expect(getByText('Add data')); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx new file mode 100644 index 00000000000000..ac7c8ae0a95c65 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiButtonEmpty, EuiHeaderLinks, EuiHeaderSectionItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useHistory } from 'react-router-dom'; +import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; +import { useGetUrlParams } from '../../../hooks'; +import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; +import { SETTINGS_ROUTE } from '../../../../common/constants'; +import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; + +const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { + defaultMessage: 'Add data', +}); + +const ANALYZE_DATA = i18n.translate('xpack.uptime.analyzeDataButtonLabel', { + defaultMessage: 'Analyze data', +}); + +const ANALYZE_MESSAGE = i18n.translate('xpack.uptime.analyzeDataButtonLabel.message', { + defaultMessage: + 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', +}); + +export function ActionMenuContent(): React.ReactElement { + const kibana = useKibana(); + const { basePath } = useUptimeSettingsContext(); + const params = useGetUrlParams(); + const { dateRangeStart, dateRangeEnd } = params; + const history = useHistory(); + + const syntheticExploratoryViewLink = createExploratoryViewUrl( + { + 'synthetics-series': { + dataType: 'synthetics', + time: { from: dateRangeStart, to: dateRangeEnd }, + } as SeriesUrl, + }, + basePath + ); + + return ( + + + + + + + + + + + {ANALYZE_MESSAGE}

    }> + + {ANALYZE_DATA} + +
    +
    + + + {ADD_DATA_LABEL} + + +
    + ); +} diff --git a/x-pack/plugins/uptime/public/components/common/header/page_header.tsx b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx index 753ce267d141ce..28a133698ae8b3 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_header.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_header.tsx @@ -12,7 +12,6 @@ import { UptimeDatePicker } from '../uptime_date_picker'; import { SyntheticsCallout } from '../../overview/synthetics_callout'; import { PageTabs } from './page_tabs'; import { CertRefreshBtn } from '../../certificates/cert_refresh_btn'; -import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { MonitorPageTitle } from '../../monitor/monitor_title'; export interface Props { @@ -52,9 +51,6 @@ export const PageHeader = ({ {showMonitorTitle && } {showTabs && }
    - - - {showCertificateRefreshBtn && } {showDatePicker && ( diff --git a/x-pack/plugins/uptime/public/components/common/header/page_tabs.test.tsx b/x-pack/plugins/uptime/public/components/common/header/page_tabs.test.tsx index edb1b56668b15c..2870006dc20be9 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_tabs.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_tabs.test.tsx @@ -15,7 +15,6 @@ describe('PageTabs', () => { const { getByText } = render(); expect(getByText('Overview')).toBeInTheDocument(); expect(getByText('Certificates')).toBeInTheDocument(); - expect(getByText('Settings')).toBeInTheDocument(); }); it('it keep params while switching', () => { @@ -32,10 +31,6 @@ describe('PageTabs', () => { 'href', '/certificates?dateRangeStart=now-10m' ); - expect(getByTestId('settings-page-link')).toHaveAttribute( - 'href', - '/settings?dateRangeStart=now-10m' - ); }); it('it resets params on overview if already on overview', () => { diff --git a/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx b/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx index 263351d4ea4d0b..74b037971c1261 100644 --- a/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/page_tabs.tsx @@ -10,7 +10,7 @@ import React, { useEffect, useState } from 'react'; import { EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useHistory, useRouteMatch } from 'react-router-dom'; -import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE, SETTINGS_ROUTE } from '../../../../common/constants'; +import { CERTIFICATES_ROUTE, OVERVIEW_ROUTE } from '../../../../common/constants'; import { useGetUrlParams } from '../../../hooks'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; @@ -28,13 +28,6 @@ const tabs = [ name: 'Certificates', dataTestSubj: 'uptimeCertificatesLink', }, - { - id: SETTINGS_ROUTE, - dataTestSubj: 'settings-page-link', - name: i18n.translate('xpack.uptime.page_header.settingsLink', { - defaultMessage: 'Settings', - }), - }, ]; export const PageTabs = () => { @@ -45,7 +38,6 @@ export const PageTabs = () => { const params = useGetUrlParams(); const isOverView = useRouteMatch(OVERVIEW_ROUTE); - const isSettings = useRouteMatch(SETTINGS_ROUTE); const isCerts = useRouteMatch(CERTIFICATES_ROUTE); useEffect(() => { @@ -55,13 +47,10 @@ export const PageTabs = () => { if (isCerts) { setSelectedTabId(CERTIFICATES_ROUTE); } - if (isSettings) { - setSelectedTabId(SETTINGS_ROUTE); - } - if (!isOverView?.isExact && !isCerts && !isSettings) { + if (!isOverView?.isExact && !isCerts) { setSelectedTabId(null); } - }, [isCerts, isSettings, isOverView]); + }, [isCerts, isOverView]); const createHrefForTab = (id: string) => { if (selectedTabId === OVERVIEW_ROUTE && id === OVERVIEW_ROUTE) { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx index 3a0a30980ab52b..da7ccd3679f57c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.test.tsx @@ -131,17 +131,18 @@ describe('WaterfallChartWrapper', () => { }); it('opens flyout on sidebar click and closes on flyout close button', async () => { - const { getByText, getAllByText, getByTestId, queryByText, getByRole } = render( + const { getByText, getByTestId, queryByText, getByRole } = render( ); - expect(getByText(`1. ${mockNetworkItems[0].url}`)).toBeInTheDocument(); + expect(getByText(`${mockNetworkItems[0].url}`)).toBeInTheDocument(); + expect(getByText(`1.`)).toBeInTheDocument(); expect(queryByText('Content type')).not.toBeInTheDocument(); expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument(); // open flyout - // selecter matches both button and accessible text. Button is the second element in the array; - const sidebarButton = getAllByText(/1./)[1]; + // selector matches both button and accessible text. Button is the second element in the array; + const sidebarButton = getByTestId(`middleTruncatedTextButton1`); fireEvent.click(sidebarButton); // check for sample flyout items @@ -163,17 +164,18 @@ describe('WaterfallChartWrapper', () => { }); it('opens flyout on sidebar click and closes on second sidebar click', async () => { - const { getByText, getAllByText, getByTestId, queryByText } = render( + const { getByText, getByTestId, queryByText } = render( ); - expect(getByText(`1. ${mockNetworkItems[0].url}`)).toBeInTheDocument(); + expect(getByText(`${mockNetworkItems[0].url}`)).toBeInTheDocument(); + expect(getByText(`1.`)).toBeInTheDocument(); expect(queryByText('Content type')).not.toBeInTheDocument(); expect(queryByText(`${mockNetworkItems[0]?.mimeType}`)).not.toBeInTheDocument(); // open flyout - // selecter matches both button and accessible text. Button is the second element in the array; - const sidebarButton = getAllByText(/1./)[1]; + // selector matches both button and accessible text. Button is the second element in the array; + const sidebarButton = getByTestId(`middleTruncatedTextButton1`); fireEvent.click(sidebarButton); // check for sample flyout items and that the flyout is focused diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx index e4c08236e49de4..0cb44bce26848b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_chart_wrapper.tsx @@ -81,6 +81,8 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { ); }, [flyoutData, isFlyoutVisible, onFlyoutClose]); + const highestSideBarIndex = Math.max(...series.map((sr) => sr.x)); + const renderSidebarItem: RenderItem = useCallback( (item) => { return ( @@ -88,10 +90,11 @@ export const WaterfallChartWrapper: React.FC = ({ data, total }) => { item={item} renderFilterScreenReaderText={hasFilters && !onlyHighlighted} onClick={onSidebarClick} + highestIndex={highestSideBarIndex} /> ); }, - [hasFilters, onlyHighlighted, onSidebarClick] + [hasFilters, onlyHighlighted, onSidebarClick, highestSideBarIndex] ); useTrackMetric({ app: 'uptime', metric: 'waterfall_chart_view', metricType: METRIC_TYPE.COUNT }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx index 4f92c882340b94..53e6583e00cc29 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_flyout.tsx @@ -77,7 +77,7 @@ export const WaterfallFlyout = ({ return null; } - const { url, details, certificates, requestHeaders, responseHeaders } = flyoutData; + const { x, url, details, certificates, requestHeaders, responseHeaders } = flyoutData; trackMetric({ metric: 'waterfall_flyout', metricType: METRIC_TYPE.CLICK }); @@ -93,7 +93,13 @@ export const WaterfallFlyout = ({

    - +

    diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.test.tsx similarity index 73% rename from x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx rename to x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.test.tsx index 7f32cac92bd9fc..2d08bbbadfbe4e 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfalll_sidebar_item.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.test.tsx @@ -13,9 +13,10 @@ import { SidebarItem } from '../waterfall/types'; import { render } from '../../../../../lib/helper/rtl_helpers'; import { WaterfallSidebarItem } from './waterfall_sidebar_item'; import { SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL } from '../../waterfall/components/translations'; +import { getChunks } from '../../waterfall/components/middle_truncated_text'; describe('waterfall filter', () => { - const url = 'http://www.elastic.co'; + const url = 'http://www.elastic.co/observability/uptime'; const index = 0; const offsetIndex = index + 1; const item: SidebarItem = { @@ -25,26 +26,34 @@ describe('waterfall filter', () => { offsetIndex, }; - it('renders sidbar item', () => { - const { getByText } = render(); + it('renders sidebar item', () => { + const { getByText } = render(); - expect(getByText(`${offsetIndex}. ${url}`)); + const chunks = getChunks(url.replace('http://www.', '')); + + expect(getByText(`${offsetIndex}. ${chunks.first}`)); + expect(getByText(`${chunks.last}`)); }); it('render screen reader text when renderFilterScreenReaderText is true', () => { const { getByLabelText } = render( - + ); expect( - getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${offsetIndex}. ${url}`) + getByLabelText(`${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ${url}`) ).toBeInTheDocument(); }); it('does not render screen reader text when renderFilterScreenReaderText is false', () => { const onClick = jest.fn(); const { getByRole } = render( - + ); const button = getByRole('button'); fireEvent.click(button); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx index be624352cd1e40..1aea9d9e69db74 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/step_detail/waterfall/waterfall_sidebar_item.tsx @@ -17,10 +17,12 @@ interface SidebarItemProps { item: SidebarItem; renderFilterScreenReaderText?: boolean; onClick?: OnSidebarClick; + highestIndex: number; } export const WaterfallSidebarItem = ({ item, + highestIndex, renderFilterScreenReaderText, onClick, }: SidebarItemProps) => { @@ -42,7 +44,8 @@ export const WaterfallSidebarItem = ({ return is400 || is500 || isSpecific300; }; - const text = `${offsetIndex}. ${item.url}`; + const text = item.url; + const ariaLabel = `${ isHighlighted && renderFilterScreenReaderText ? `${SIDEBAR_FILTER_MATCHES_SCREENREADER_LABEL} ` @@ -58,11 +61,13 @@ export const WaterfallSidebarItem = ({ @@ -70,11 +75,13 @@ export const WaterfallSidebarItem = ({ diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx index de352186e26fda..5006ad927f313c 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.test.tsx @@ -5,9 +5,10 @@ * 2.0. */ -import { getChunks, MiddleTruncatedText } from './middle_truncated_text'; -import { render, within, fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; +import { within, fireEvent, waitFor } from '@testing-library/react'; +import { getChunks, MiddleTruncatedText } from './middle_truncated_text'; +import { render } from '../../../../../lib/helper/rtl_helpers'; const longString = 'this-is-a-really-really-really-really-really-really-really-really-long-string.madeup.extension'; @@ -28,7 +29,14 @@ describe('Component', () => { const url = 'http://www.elastic.co'; it('renders truncated text and aria label', () => { const { getByText, getByLabelText } = render( - + ); expect(getByText(first)).toBeInTheDocument(); @@ -39,7 +47,13 @@ describe('Component', () => { it('renders screen reader only text', () => { const { getByTestId } = render( - + ); const { getByText } = within(getByTestId('middleTruncatedTextSROnly')); @@ -49,7 +63,13 @@ describe('Component', () => { it('renders external link', () => { const { getByText } = render( - + ); const link = getByText('Open resource in new tab').closest('a'); @@ -61,13 +81,15 @@ describe('Component', () => { const handleClick = jest.fn(); const { getByTestId } = render( ); - const button = getByTestId('middleTruncatedTextButton'); + const button = getByTestId('middleTruncatedTextButton1'); fireEvent.click(button); await waitFor(() => { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx index 6a9d6660c901c7..cb7159485856c4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/middle_truncated_text.tsx @@ -6,12 +6,22 @@ */ import React, { useMemo } from 'react'; -import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiScreenReaderOnly, EuiToolTip, EuiButtonEmpty, EuiLink } from '@elastic/eui'; +import { + EuiScreenReaderOnly, + EuiToolTip, + EuiButtonEmpty, + EuiLink, + EuiText, + EuiIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FIXED_AXIS_HEIGHT } from './constants'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; interface Props { + index: number; + highestIndex: number; ariaLabel: string; text: string; onClick?: (event: React.MouseEvent) => void; @@ -19,7 +29,7 @@ interface Props { url: string; } -const OuterContainer = styled.span` +const OuterContainer = euiStyled.span` position: relative; display: inline-flex; align-items: center; @@ -28,13 +38,21 @@ const OuterContainer = styled.span` } `; // NOTE: min-width: 0 ensures flexbox and no-wrap children can co-exist -const InnerContainer = styled.span` +const InnerContainer = euiStyled.span` overflow: hidden; display: flex; align-items: center; `; -const FirstChunk = styled.span` +const IndexNumber = euiStyled(EuiText)` + font-family: ${(props) => props.theme.eui.euiCodeFontFamily}; + margin-right: ${(props) => props.theme.eui.euiSizeXS}; + line-height: ${FIXED_AXIS_HEIGHT}px; + text-align: right; + background-color: ${(props) => props.theme.eui.euiColorLightestShade}; +`; + +const FirstChunk = euiStyled.span` text-overflow: ellipsis; white-space: nowrap; overflow: hidden; @@ -42,13 +60,13 @@ const FirstChunk = styled.span` text-align: left; `; // safari doesn't auto align text left in some cases -const LastChunk = styled.span` +const LastChunk = euiStyled.span` flex-shrink: 0; line-height: ${FIXED_AXIS_HEIGHT}px; text-align: left; `; // safari doesn't auto align text left in some cases -const StyledButton = styled(EuiButtonEmpty)` +const StyledButton = euiStyled(EuiButtonEmpty)` &&& { border: none; @@ -59,6 +77,10 @@ const StyledButton = styled(EuiButtonEmpty)` } `; +const SecureIcon = euiStyled(EuiIcon)` + margin-right: ${(props) => props.theme.eui.euiSizeXS}; +`; + export const getChunks = (text: string = '') => { const END_CHARS = 12; const chars = text.split(''); @@ -70,7 +92,18 @@ export const getChunks = (text: string = '') => { // Helper component for adding middle text truncation, e.g. // really-really-really-long....ompressed.js // Can be used to accomodate content in sidebar item rendering. -export const MiddleTruncatedText = ({ ariaLabel, text, onClick, setButtonRef, url }: Props) => { +export const MiddleTruncatedText = ({ + index, + ariaLabel, + text: fullText, + onClick, + setButtonRef, + url, + highestIndex, +}: Props) => { + const secureHttps = fullText.startsWith('https://'); + const text = fullText.replace(/https:\/\/www.|http:\/\/www.|http:\/\/|https:\/\//, ''); + const chunks = useMemo(() => { return getChunks(text); }, [text]); @@ -78,24 +111,44 @@ export const MiddleTruncatedText = ({ ariaLabel, text, onClick, setButtonRef, ur return ( - {text} + {fullText} - + <> {onClick ? ( + + {index + '.'} + + {secureHttps && ( + + )} {chunks.first} {chunks.last} ) : ( - {chunks.first} + + {index}. {chunks.first} + {chunks.last} )} diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts index e8125ebcf30cb4..9a884c6824284b 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/styles.ts @@ -90,6 +90,7 @@ export const WaterfallChartSidebarWrapper = euiStyled(EuiFlexItem)` export const WaterfallChartSidebarContainer = euiStyled.div` height: ${(props) => `${props.height}px`}; overflow-y: hidden; + overflow-x: hidden; `; export const WaterfallChartSidebarContainerInnerPanel: StyledComponent< @@ -107,8 +108,7 @@ export const WaterfallChartSidebarContainerFlexGroup = euiStyled(EuiFlexGroup)` // Ensures flex items honour no-wrap of children, rather than trying to extend to the full width of children. export const WaterfallChartSidebarFlexItem = euiStyled(EuiFlexItem)` min-width: 0; - padding-left: ${(props) => props.theme.eui.paddingSizes.m}; - padding-right: ${(props) => props.theme.eui.paddingSizes.m}; + padding-right: ${(props) => props.theme.eui.paddingSizes.s}; justify-content: space-around; `; diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 0afe804de9717b..82ba70155608c8 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -50,7 +50,12 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor read: [], }, alerting: { - all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + rule: { + all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + }, + alert: { + all: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + }, }, management: { insightsAndAlerting: ['triggersActions'], @@ -66,7 +71,12 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor read: [umDynamicSettings.name], }, alerting: { - read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + rule: { + read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + }, + alert: { + read: ['xpack.uptime.alerts.tls', 'xpack.uptime.alerts.monitorStatus'], + }, }, management: { insightsAndAlerting: ['triggersActions'], diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 8647c5951b7f35..c56e8adfbe34fb 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -19,10 +19,11 @@ interface CreateTestConfigOptions { disabledPlugins?: string[]; ssl?: boolean; enableActionsProxy: boolean; - rejectUnauthorized?: boolean; + verificationMode?: 'full' | 'none' | 'certificate'; publicBaseUrl?: boolean; preconfiguredAlertHistoryEsIndex?: boolean; customizeLocalHostTls?: boolean; + rejectUnauthorized?: boolean; // legacy } // test.not-enabled is specifically not enabled @@ -49,9 +50,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) license = 'trial', disabledPlugins = [], ssl = false, - rejectUnauthorized = true, + verificationMode = 'full', preconfiguredAlertHistoryEsIndex = false, customizeLocalHostTls = false, + rejectUnauthorized = true, // legacy } = options; return async ({ readConfigFile }: FtrConfigProviderContext) => { @@ -101,19 +103,19 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) { url: tlsWebhookServers.rejectUnauthorizedFalse, tls: { - rejectUnauthorized: false, + verificationMode: 'none', }, }, { url: tlsWebhookServers.rejectUnauthorizedTrue, tls: { - rejectUnauthorized: true, + verificationMode: 'full', }, }, { url: tlsWebhookServers.caFile, tls: { - rejectUnauthorized: true, + verificationMode: 'certificate', certificateAuthoritiesFiles: [CA_CERT_PATH], }, }, @@ -151,6 +153,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) '--xpack.alerting.invalidateApiKeysTask.interval="15s"', `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, `--xpack.actions.rejectUnauthorized=${rejectUnauthorized}`, + `--xpack.actions.tls.verificationMode=${verificationMode}`, ...actionsProxyUrl, ...customHostSettings, '--xpack.eventLog.logEntries=true', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts index 9a7cd8d333b44a..e98b7af075d643 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts/server/plugin.ts @@ -69,21 +69,23 @@ export class FixturePlugin implements Plugin { + const { body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'MY action', + connector_type_id: 'test.noop', + config: {}, + secrets: {}, + }) + .expect(200); + + const lotsOfSpaces = ''.padEnd(100 * 1000); // 100K space chars + const response = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send( + getTestAlertData({ + params: { + ignoredButPersisted: lotsOfSpaces, + }, + actions: [ + { + id: createdAction.id, + group: 'default', + params: {}, + }, + ], + }) + ); + + expect(response.status).to.eql(200); + objectRemover.add(Spaces.space1.id, response.body.id, 'rule', 'alerting'); + + expect(response.body.params.ignoredButPersisted).to.eql(lotsOfSpaces); + + // Ensure AAD isn't broken + await checkAAD({ + supertest, + spaceId: Spaces.space1.id, + type: 'alert', + id: response.body.id, + }); + }); + it('should allow providing custom saved object ids (uuid v1)', async () => { const customId = '09570bb0-6299-11eb-8fde-9fe5ce6ea450'; const response = await supertest diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts new file mode 100644 index 00000000000000..511e97b96e35d7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/config.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + disabledPlugins: ['security'], + license: 'trial', + enableActionsProxy: false, + rejectUnauthorized: false, + verificationMode: undefined, + customizeLocalHostTls: true, + preconfiguredAlertHistoryEsIndex: true, +}); diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/scenarios.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/scenarios.ts new file mode 100644 index 00000000000000..5c00ad2f4f70f5 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/scenarios.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 { Space } from '../common/types'; + +const Space1: Space = { + id: 'space1', + namespace: 'space1', + name: 'Space 1', + disabledFeatures: [], +}; + +const Other: Space = { + id: 'other', + namespace: 'other', + name: 'Other', + disabledFeatures: [], +}; + +const Default: Space = { + id: 'default', + namespace: undefined, + name: 'Default', + disabledFeatures: [], +}; + +export const Spaces = { + space1: Space1, + other: Other, + default: Default, +}; diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.ts new file mode 100644 index 00000000000000..4af33136cd42c0 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/actions/builtin_action_types/webhook.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 http from 'http'; +import https from 'https'; +import getPort from 'get-port'; +import expect from '@kbn/expect'; +import { URL, format as formatUrl } from 'url'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + getWebhookServer, + getHttpsWebhookServer, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { createTlsWebhookServer } from '../../../../common/lib/get_tls_webhook_servers'; + +// eslint-disable-next-line import/no-default-export +export default function webhookTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + async function createWebhookAction( + webhookSimulatorURL: string, + config: Record> = {} + ): Promise { + const url = formatUrl(new URL(webhookSimulatorURL), { auth: false }); + const composedConfig = { + headers: { + 'Content-Type': 'text/plain', + }, + ...config, + url, + }; + + const { body: createdAction } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'test') + .send({ + name: 'A generic Webhook action', + actionTypeId: '.webhook', + secrets: {}, + config: composedConfig, + }) + .expect(200); + + return createdAction.id; + } + + async function getPortOfConnector(connectorId: string): Promise { + const response = await supertest.get(`/api/actions/connectors`).expect(200); + const connector = response.body.find((conn: { id: string }) => conn.id === connectorId); + if (connector === undefined) { + throw new Error(`unable to find connector with id ${connectorId}`); + } + + // server URL is the connector name + const url = connector.name; + const parsedUrl = new URL(url); + return parsedUrl.port; + } + + describe('webhook action', () => { + describe('with http endpoint', () => { + let webhookSimulatorURL: string = ''; + let webhookServer: http.Server; + before(async () => { + webhookServer = await getWebhookServer(); + const availablePort = await getPort({ port: 9000 }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `http://localhost:${availablePort}`; + }); + + it('webhook can be executed without username and password', async () => { + const webhookActionId = await createWebhookAction(webhookSimulatorURL); + const { body: result } = await supertest + .post(`/api/actions/action/${webhookActionId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'success', + }, + }) + .expect(200); + + expect(result.status).to.eql('ok'); + }); + + after(() => { + webhookServer.close(); + }); + }); + + describe('with https endpoint and rejectUnauthorized=false', () => { + let webhookSimulatorURL: string = ''; + let webhookServer: https.Server; + + before(async () => { + webhookServer = await getHttpsWebhookServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + webhookServer.listen(availablePort); + webhookSimulatorURL = `https://localhost:${availablePort}`; + }); + + it('should support the POST method against webhook target', async () => { + const webhookActionId = await createWebhookAction(webhookSimulatorURL, { method: 'post' }); + const { body: result } = await supertest + .post(`/api/actions/action/${webhookActionId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'success_post_method', + }, + }) + .expect(200); + + expect(result.status).to.eql('ok'); + }); + + after(() => { + webhookServer.close(); + }); + }); + + describe('tls customization', () => { + it('should handle the xpack.actions.rejectUnauthorized: false', async () => { + const connectorId = 'custom.tls.noCustom'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/${connectorId}/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('ok'); + }); + + it('should handle the customized rejectUnauthorized: false', async () => { + const connectorId = 'custom.tls.rejectUnauthorizedFalse'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/custom.tls.rejectUnauthorizedFalse/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('ok'); + }); + + it('should handle the customized rejectUnauthorized: true', async () => { + const connectorId = 'custom.tls.rejectUnauthorizedTrue'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/custom.tls.rejectUnauthorizedTrue/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('error'); + expect(body.service_message.indexOf('certificate')).to.be.greaterThan(0); + }); + + it('should handle the customized ca file', async () => { + const connectorId = 'custom.tls.caFile'; + const port = await getPortOfConnector(connectorId); + const server = await createTlsWebhookServer(port); + const { status, body } = await supertest + .post(`/api/actions/connector/custom.tls.caFile/_execute`) + .set('kbn-xsrf', 'test') + .send({ + params: { + body: 'foo', + }, + }); + expect(status).to.eql(200); + server.close(); + + expect(body.status).to.eql('ok'); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts new file mode 100644 index 00000000000000..a5a046dcbbe86f --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only_legacy/tests/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { Spaces } from '../scenarios'; + +// eslint-disable-next-line import/no-default-export +export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { + describe('alerting api integration spaces only legacy configuration', function () { + this.tags('ciGroup12'); + + loadTestFile(require.resolve('./actions/builtin_action_types/webhook')); + }); +} + +export async function buildUp(getService: FtrProviderContext['getService']) { + const spacesService = getService('spaces'); + for (const space of Object.values(Spaces)) { + if (space.id === 'default') continue; + + const { id, name, disabledFeatures } = space; + await spacesService.create({ id, name, disabledFeatures }); + } +} + +export async function tearDown(getService: FtrProviderContext['getService']) { + const esArchiver = getService('esArchiver'); + await esArchiver.unload('empty_kibana'); +} diff --git a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js b/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js deleted file mode 100644 index c11bdb7112640a..00000000000000 --- a/x-pack/test/api_integration/apis/beats/assign_tags_to_beats.js +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { ES_INDEX_NAME } from './constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const es = getService('legacyEs'); - const randomness = getService('randomness'); - - describe('assign_tags_to_beats', () => { - const archive = 'beats/list'; - - beforeEach('load beats archive', () => esArchiver.load(archive)); - afterEach('unload beats archive', () => esArchiver.unload(archive)); - - it('should add a single tag to a single beat', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [{ beatId: 'bar', tag: 'production' }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([{ success: true, result: { message: 'updated' } }]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - const beat = esResponse._source.beat; - expect(beat.tags).to.eql(['production']); - }); - - it('should not re-add an existing tag to a beat', async () => { - const tags = ['production']; - - let esResponse; - let beat; - - // Before adding the existing tag - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql([...tags, 'qa']); - - // Adding the existing tag - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [{ beatId: 'foo', tag: 'production' }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([{ success: true, result: { message: 'updated' } }]); - - // After adding the existing tag - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql([...tags, 'qa']); - }); - - it('should add a single tag to a multiple beats', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [ - { beatId: 'foo', tag: 'development' }, - { beatId: 'bar', tag: 'development' }, - ], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - let esResponse; - let beat; - - // Beat foo - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // Beat bar - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql(['development']); - }); - - it('should add multiple tags to a single beat', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [ - { beatId: 'bar', tag: 'development' }, - { beatId: 'bar', tag: 'production' }, - ], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - const beat = esResponse._source.beat; - expect(beat.tags).to.eql(['development', 'production']); - }); - - it('should add multiple tags to a multiple beats', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [ - { beatId: 'foo', tag: 'development' }, - { beatId: 'bar', tag: 'production' }, - ], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - let esResponse; - let beat; - - // Beat foo - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql(['production', 'qa', 'development']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // Beat bar - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql(['production']); - }); - - it('should return errors for non-existent beats', async () => { - const nonExistentBeatId = randomness.word(); - - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [{ beatId: nonExistentBeatId, tag: 'production' }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: false, error: { code: 404, message: `Beat ${nonExistentBeatId} not found` } }, - ]); - }); - - it('should return errors for non-existent tags', async () => { - const nonExistentTag = randomness.word(); - - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [{ beatId: 'bar', tag: nonExistentTag }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: false, error: { code: 404, message: `Tag ${nonExistentTag} not found` } }, - ]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - const beat = esResponse._source.beat; - expect(beat).to.not.have.property('tags'); - }); - - it('should return errors for non-existent beats and tags', async () => { - const nonExistentBeatId = randomness.word(); - const nonExistentTag = randomness.word(); - - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/assignments') - .set('kbn-xsrf', 'xxx') - .send({ - assignments: [{ beatId: nonExistentBeatId, tag: nonExistentTag }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { - success: false, - error: { - code: 404, - message: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found`, - }, - }, - ]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - const beat = esResponse._source.beat; - expect(beat).to.not.have.property('tags'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/constants.js b/x-pack/test/api_integration/apis/beats/constants.js deleted file mode 100644 index 9e8064f4d1b5b1..00000000000000 --- a/x-pack/test/api_integration/apis/beats/constants.js +++ /dev/null @@ -1,8 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const ES_INDEX_NAME = '.management-beats'; diff --git a/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js b/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js deleted file mode 100644 index 5ac59b690465fb..00000000000000 --- a/x-pack/test/api_integration/apis/beats/create_enrollment_tokens.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import moment from 'moment'; -import { ES_INDEX_NAME } from './constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const es = getService('legacyEs'); - - describe('create_enrollment_token', () => { - it('should create one token by default', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/enrollment_tokens') - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - - const tokensFromApi = apiResponse.results.map((r) => r.item); - - const esResponse = await es.search({ - index: ES_INDEX_NAME, - q: 'type:enrollment_token', - }); - - const tokensInEs = esResponse.hits.hits.map((hit) => hit._source.enrollment_token.token); - - expect(tokensFromApi.length).to.eql(1); - expect(tokensFromApi).to.eql(tokensInEs); - }); - - it('should create the specified number of tokens', async () => { - const numTokens = 1000; - - const { body: apiResponse } = await supertest - .post('/api/beats/enrollment_tokens') - .set('kbn-xsrf', 'xxx') - .send({ - num_tokens: numTokens, - }) - .expect(200); - - const tokensFromApi = apiResponse.results.map((r) => r.item); - - const esResponse = await es.search({ - index: ES_INDEX_NAME, - q: 'type:enrollment_token', - size: numTokens, - }); - - const tokensInEs = esResponse.hits.hits.map((hit) => hit._source.enrollment_token.token); - - expect(tokensFromApi).to.be.an('array'); - expect(tokensFromApi.length).to.eql(numTokens); - expect(tokensInEs.length).to.eql(numTokens); - expect(tokensFromApi.sort()).to.eql(tokensInEs.sort()); - }); - - it('should set token expiration to 10 minutes from now by default', async () => { - await supertest - .post('/api/beats/enrollment_tokens') - .set('kbn-xsrf', 'xxx') - .send() - .expect(200); - - const esResponse = await es.search({ - index: ES_INDEX_NAME, - q: 'type:enrollment_token', - }); - - const tokenInEs = esResponse.hits.hits[0]._source.enrollment_token; - - // We do a fuzzy check to see if the token expires between 9 and 10 minutes - // from now because a bit of time has elapsed been the creation of the - // tokens and this check. - const tokenExpiresOn = moment(tokenInEs.expires_on).valueOf(); - const tenMinutesFromNow = moment().add('10', 'minutes').valueOf(); - const almostTenMinutesFromNow = moment(tenMinutesFromNow).subtract('2', 'seconds').valueOf(); - expect(tokenExpiresOn).to.be.lessThan(tenMinutesFromNow); - expect(tokenExpiresOn).to.be.greaterThan(almostTenMinutesFromNow); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/enroll_beat.js b/x-pack/test/api_integration/apis/beats/enroll_beat.js deleted file mode 100644 index a1ac449e7fcb85..00000000000000 --- a/x-pack/test/api_integration/apis/beats/enroll_beat.js +++ /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 expect from '@kbn/expect'; -import moment from 'moment'; - -import { ES_INDEX_NAME } from './constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const randomness = getService('randomness'); - const es = getService('legacyEs'); - - describe('enroll_beat', () => { - let validEnrollmentToken; - let beatId; - let beat; - - beforeEach(async () => { - validEnrollmentToken = randomness.word(); - - beatId = randomness.word(); - const version = - randomness.integer({ min: 1, max: 10 }) + - '.' + - randomness.integer({ min: 1, max: 10 }) + - '.' + - randomness.integer({ min: 1, max: 10 }); - - beat = { - type: 'filebeat', - host_name: 'foo.bar.com', - name: randomness.word(), - version, - }; - - await es.index({ - index: ES_INDEX_NAME, - id: `enrollment_token:${validEnrollmentToken}`, - body: { - type: 'enrollment_token', - enrollment_token: { - token: validEnrollmentToken, - expires_on: moment().add(4, 'hours').toJSON(), - }, - }, - }); - }); - - it('should enroll beat in a verified state', async () => { - await supertest - .post(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-enrollment-token', validEnrollmentToken) - .send(beat) - .expect(200); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:${beatId}`, - }); - - expect(esResponse._source.beat).to.have.property('verified_on'); - expect(esResponse._source.beat).to.have.property('host_ip'); - }); - - it('should contain an access token in the response', async () => { - const { body: apiResponse } = await supertest - .post(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-enrollment-token', validEnrollmentToken) - .send(beat) - .expect(200); - - const accessTokenFromApi = apiResponse.item; - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:${beatId}`, - }); - - const accessTokenInEs = esResponse._source.beat.access_token; - - expect(accessTokenFromApi.length).to.be.greaterThan(0); - expect(accessTokenFromApi).to.eql(accessTokenInEs); - }); - - it('should reject an invalid enrollment token', async () => { - const { body: apiResponse } = await supertest - .post(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-enrollment-token', randomness.word()) - .send(beat) - .expect(400); - - expect(apiResponse).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: 'Invalid enrollment token', - }); - }); - - it('should reject an expired enrollment token', async () => { - const expiredEnrollmentToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + - 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1LCJleHAiOjE1MzAzMzAxMzV9.' + - 'Azf4czAwWZEflR7Pf8pi-DUTcve9xyxWyViNYeUSGog'; - - await es.index({ - index: ES_INDEX_NAME, - id: `enrollment_token:${expiredEnrollmentToken}`, - body: { - type: 'enrollment_token', - enrollment_token: { - token: expiredEnrollmentToken, - expires_on: moment().subtract(1, 'minute').toJSON(), - }, - }, - }); - - const { body: apiResponse } = await supertest - .post(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-enrollment-token', expiredEnrollmentToken) - .send(beat) - .expect(400); - - expect(apiResponse).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: 'Expired enrollment token', - }); - }); - - it('should delete the given enrollment token so it may not be reused', async () => { - await supertest - .post(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-enrollment-token', validEnrollmentToken) - .send(beat) - .expect(200); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `enrollment_token:${validEnrollmentToken}`, - ignore: [404], - }); - - expect(esResponse.found).to.be(false); - }); - - it('should fail if the beat with the same ID is enrolled twice', async () => { - await supertest - .post(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-enrollment-token', validEnrollmentToken) - .send(beat) - .expect(200); - - await es.index({ - index: ES_INDEX_NAME, - id: `enrollment_token:${validEnrollmentToken}`, - body: { - type: 'enrollment_token', - enrollment_token: { - token: validEnrollmentToken, - expires_on: moment().add(4, 'hours').toJSON(), - }, - }, - }); - - await supertest - .post(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-enrollment-token', validEnrollmentToken) - .send(beat) - .expect(200); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/get_beat.js b/x-pack/test/api_integration/apis/beats/get_beat.js deleted file mode 100644 index 610ea72bb3d423..00000000000000 --- a/x-pack/test/api_integration/apis/beats/get_beat.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { ES_INDEX_NAME } from './constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const es = getService('legacyEs'); - - describe('get_beat_configuration', () => { - const archive = 'beats/list'; - - beforeEach('load beats archive', () => esArchiver.load(archive)); - afterEach('unload beats archive', () => esArchiver.unload(archive)); - - it('should return no configurations for the beat without tags', async () => { - await es.index({ - index: ES_INDEX_NAME, - id: `beat:empty`, - body: { - type: 'beat', - beat: { - type: 'filebeat', - active: true, - host_ip: '1.2.3.4', - host_name: 'empty.com', - id: 'empty', - name: 'empty_filebeat', - access_token: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI', // eslint-disable-line - }, - }, - }); - - const { body: apiResponse } = await supertest - .get('/api/beats/agent/empty/configuration') - .set( - 'kbn-beats-access-token', - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + - 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + - 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' - ) - .expect(200); - - const configurationBlocks = apiResponse.list; - - expect(configurationBlocks).to.be.an(Array); - expect(configurationBlocks.length).to.be(0); - }); - - it('should return merged configuration for the beat', async () => { - const { body: apiResponse } = await supertest - .get('/api/beats/agent/foo/configuration') - .set( - 'kbn-beats-access-token', - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + - 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + - 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' - ) - .expect(200); - - const configurationBlocks = apiResponse.list; - - expect(configurationBlocks).to.be.an(Array); - expect(configurationBlocks.length).to.be(3); - - expect(configurationBlocks[1].type).to.be('metricbeat.modules'); - expect(configurationBlocks[1].config).to.eql({ - module: 'memcached', - hosts: ['localhost:11211'], - }); - - expect(configurationBlocks[2].type).to.be('metricbeat.modules'); - expect(configurationBlocks[2].config).to.eql({ - module: 'memcached', - hosts: ['localhost:4949'], - 'node.namespace': 'node', - }); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/index.js b/x-pack/test/api_integration/apis/beats/index.js deleted file mode 100644 index b490ddd24da388..00000000000000 --- a/x-pack/test/api_integration/apis/beats/index.js +++ /dev/null @@ -1,31 +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 { ES_INDEX_NAME } from './constants'; - -export default function ({ getService, loadTestFile }) { - const es = getService('legacyEs'); - - describe('beats', () => { - const cleanup = () => - es.indices.delete({ - index: ES_INDEX_NAME, - ignore: [404], - }); - - beforeEach(cleanup); - - loadTestFile(require.resolve('./create_enrollment_tokens')); - loadTestFile(require.resolve('./enroll_beat')); - loadTestFile(require.resolve('./list_beats')); - loadTestFile(require.resolve('./update_beat')); - loadTestFile(require.resolve('./set_tag')); - loadTestFile(require.resolve('./assign_tags_to_beats')); - loadTestFile(require.resolve('./remove_tags_from_beats')); - loadTestFile(require.resolve('./get_beat')); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/list_beats.js b/x-pack/test/api_integration/apis/beats/list_beats.js deleted file mode 100644 index a844b814f7ca8c..00000000000000 --- a/x-pack/test/api_integration/apis/beats/list_beats.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('list_beats', () => { - const archive = 'beats/list'; - - beforeEach('load beats archive', () => esArchiver.load(archive)); - afterEach('unload beats archive', () => esArchiver.unload(archive)); - - it('should return all beats', async () => { - const { body: apiResponse } = await supertest.get('/api/beats/agents').expect(200); - - const beatsFromApi = apiResponse.list; - - expect(beatsFromApi.length).to.be(4); - expect(beatsFromApi.filter((beat) => beat.hasOwnProperty('verified_on')).length).to.be(1); - expect(beatsFromApi.find((beat) => beat.hasOwnProperty('verified_on')).id).to.be('foo'); - }); - - it('should not return access tokens', async () => { - const { body: apiResponse } = await supertest.get('/api/beats/agents').expect(200); - - const beatsFromApi = apiResponse.list; - - expect(beatsFromApi.length).to.be(4); - expect(beatsFromApi.filter((beat) => beat.hasOwnProperty('access_token')).length).to.be(0); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js b/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js deleted file mode 100644 index 1c9ce2ffb7d484..00000000000000 --- a/x-pack/test/api_integration/apis/beats/remove_tags_from_beats.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { ES_INDEX_NAME } from './constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const es = getService('legacyEs'); - const randomness = getService('randomness'); - - describe('remove_tags_from_beats', () => { - const archive = 'beats/list'; - - beforeEach('load beats archive', () => esArchiver.load(archive)); - afterEach('unload beats archive', () => esArchiver.unload(archive)); - - it('should remove a single tag from a single beat', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/removals') - .set('kbn-xsrf', 'xxx') - .send({ - removals: [{ beatId: 'foo', tag: 'production' }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([{ success: true, result: { message: 'updated' } }]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - const beat = esResponse._source.beat; - expect(beat.tags).to.eql(['qa']); - }); - - it('should remove a single tag from a multiple beats', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/removals') - .set('kbn-xsrf', 'xxx') - .send({ - removals: [ - { beatId: 'foo', tag: 'development' }, - { beatId: 'bar', tag: 'development' }, - ], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - let esResponse; - let beat; - - // Beat foo - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql(['production', 'qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // Beat bar - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - beat = esResponse._source.beat; - expect(beat).to.not.have.property('tags'); - }); - - it('should remove multiple tags from a single beat', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/removals') - .set('kbn-xsrf', 'xxx') - .send({ - removals: [ - { beatId: 'foo', tag: 'development' }, - { beatId: 'foo', tag: 'production' }, - ], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - const beat = esResponse._source.beat; - expect(beat.tags).to.eql(['qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - }); - - it('should remove multiple tags from a multiple beats', async () => { - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/removals') - .set('kbn-xsrf', 'xxx') - .send({ - removals: [ - { beatId: 'foo', tag: 'production' }, - { beatId: 'bar', tag: 'development' }, - ], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: true, result: { message: 'updated' } }, - { success: true, result: { message: 'updated' } }, - ]); - - let esResponse; - let beat; - - // Beat foo - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:foo`, - }); - - beat = esResponse._source.beat; - expect(beat.tags).to.eql(['qa']); // as beat 'foo' already had 'production' and 'qa' tags attached to it - - // Beat bar - esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - beat = esResponse._source.beat; - expect(beat).to.not.have.property('tags'); - }); - - it('should return errors for non-existent beats', async () => { - const nonExistentBeatId = randomness.word(); - - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/removals') - .set('kbn-xsrf', 'xxx') - .send({ - removals: [{ beatId: nonExistentBeatId, tag: 'production' }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: false, error: { code: 404, message: `Beat ${nonExistentBeatId} not found` } }, - ]); - }); - - it('should return errors for non-existent tags', async () => { - const nonExistentTag = randomness.word(); - - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/removals') - .set('kbn-xsrf', 'xxx') - .send({ - removals: [{ beatId: 'bar', tag: nonExistentTag }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { success: false, error: { code: 404, message: `Tag ${nonExistentTag} not found` } }, - ]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - const beat = esResponse._source.beat; - expect(beat).to.not.have.property('tags'); - }); - - it('should return errors for non-existent beats and tags', async () => { - const nonExistentBeatId = randomness.word(); - const nonExistentTag = randomness.word(); - - const { body: apiResponse } = await supertest - .post('/api/beats/agents_tags/removals') - .set('kbn-xsrf', 'xxx') - .send({ - removals: [{ beatId: nonExistentBeatId, tag: nonExistentTag }], - }) - .expect(200); - - expect(apiResponse.results).to.eql([ - { - success: false, - error: { - code: 404, - message: `Beat ${nonExistentBeatId} and tag ${nonExistentTag} not found`, - }, - }, - ]); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `beat:bar`, - }); - - const beat = esResponse._source.beat; - expect(beat).to.not.have.property('tags'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/set_config.js b/x-pack/test/api_integration/apis/beats/set_config.js deleted file mode 100644 index fbc3db30313bfb..00000000000000 --- a/x-pack/test/api_integration/apis/beats/set_config.js +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { ES_INDEX_NAME } from './constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const es = getService('legacyEs'); - const esArchiver = getService('esArchiver'); - - describe('set_config', () => { - const archive = 'beats/list'; - - beforeEach('load archive', () => esArchiver.load(archive)); - afterEach('unload archive', () => esArchiver.unload(archive)); - - it('should create a configuration block', async () => { - const tagId = 'qa'; - await supertest - .put(`/api/beats/configurations`) - .set('kbn-xsrf', 'xxx') - .send([ - { - type: 'output', - description: 'smething', - config: { elasticsearch: { hosts: ['localhost:9200'], username: 'foo' } }, - }, - ]) - .expect(200); - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `tag:${tagId}`, - }); - - const tagInEs = esResponse._source; - - expect(tagInEs.type).to.be('tag'); - expect(tagInEs.tag.id).to.be(tagId); - expect(tagInEs.tag.configuration_blocks).to.be.an(Array); - expect(tagInEs.tag.configuration_blocks.length).to.be(1); - expect(tagInEs.tag.configuration_blocks[0].type).to.be('output'); - expect(tagInEs.tag.configuration_blocks[0].configs).to.eql([ - { - elasticsearch: { - hosts: ['localhost:9200'], - username: 'foo', - }, - }, - ]); - }); - - // it('should create a tag with two configuration blocks', async () => { - // const tagId = 'production'; - // await supertest - // .put(`/api/beats/tag/${tagId}`) - // .set('kbn-xsrf', 'xxx') - // .send({ - // configuration_blocks: [ - // { - // type: 'filebeat.inputs', - // configs: [ - // { - // paths: ['./foo'], - // }, - // ], - // }, - // { - // type: 'output', - // configs: [ - // { - // elasticsearch: { - // hosts: ['localhost:9200'], - // username: 'foo', - // }, - // }, - // ], - // }, - // ], - // }) - // .expect(201); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `tag:${tagId}`, - // }); - - // const tagInEs = esResponse._source; - - // expect(tagInEs.type).to.be('tag'); - // expect(tagInEs.tag.id).to.be(tagId); - // expect(tagInEs.tag.configuration_blocks).to.be.an(Array); - // expect(tagInEs.tag.configuration_blocks.length).to.be(2); - // expect(tagInEs.tag.configuration_blocks[0].type).to.be('filebeat.inputs'); - // expect(tagInEs.tag.configuration_blocks[0].configs).to.eql([ - // { - // paths: ['./foo'], - // }, - // ]); - // expect(tagInEs.tag.configuration_blocks[1].type).to.be('output'); - // expect(tagInEs.tag.configuration_blocks[1].configs).to.eql([ - // { - // elasticsearch: { - // hosts: ['localhost:9200'], - // username: 'foo', - // }, - // }, - // ]); - // }); - - // it('should fail when creating a tag with two configuration blocks of type output', async () => { - // const tagId = 'production'; - // await supertest - // .put(`/api/beats/tag/${tagId}`) - // .set('kbn-xsrf', 'xxx') - // .send({ - // configuration_blocks: [ - // { - // type: 'output', - // configs: [ - // { - // elasticsearch: { - // hosts: ['localhost:9200'], - // username: 'foo', - // }, - // }, - // ], - // }, - // { - // type: 'output', - // configs: [ - // { - // elasticsearch: { - // hosts: ['localhost:9200'], - // username: 'foo', - // }, - // }, - // ], - // }, - // ], - // }) - // .expect(400); - // }); - - // it('should fail when creating a tag with an invalid configuration block type', async () => { - // const tagId = 'production'; - // await supertest - // .put(`/api/beats/tag/${tagId}`) - // .set('kbn-xsrf', 'xxx') - // .send({ - // configuration_blocks: [ - // { - // type: chance.word(), - // configs: [ - // { - // elasticsearch: { - // hosts: ['localhost:9200'], - // username: 'foo', - // }, - // }, - // ], - // }, - // ], - // }) - // .expect(400); - // }); - - // it('should update an existing tag', async () => { - // const tagId = 'production'; - // await supertest - // .put(`/api/beats/tag/${tagId}`) - // .set('kbn-xsrf', 'xxx') - // .send({ - // configuration_blocks: [ - // { - // type: 'filebeat.inputs', - // configs: [ - // { - // paths: ['./test'], - // }, - // ], - // }, - // { - // type: 'output', - // configs: [ - // { - // elasticsearch: { - // hosts: ['localhost:9200'], - // username: 'foo', - // }, - // }, - // ], - // }, - // ], - // }) - // .expect(201); - - // await supertest - // .put(`/api/beats/tag/${tagId}`) - // .set('kbn-xsrf', 'xxx') - // .send({ - // configuration_blocks: [ - // { - // type: 'output', - // configs: [ - // { - // elasticsearch: { - // hosts: ['localhost:9000'], - // username: 'foo', - // }, - // }, - // ], - // }, - // ], - // }) - // .expect(200); - - // const esResponse = await es.get({ - // index: ES_INDEX_NAME, - // type: ES_TYPE_NAME, - // id: `tag:${tagId}`, - // }); - - // const tagInEs = esResponse._source; - - // expect(tagInEs.type).to.be('tag'); - // expect(tagInEs.tag.id).to.be(tagId); - // expect(tagInEs.tag.configuration_blocks).to.be.an(Array); - // expect(tagInEs.tag.configuration_blocks.length).to.be(1); - // expect(tagInEs.tag.configuration_blocks[0].type).to.be('output'); - // expect(tagInEs.tag.configuration_blocks[0].configs).to.eql([ - // { - // elasticsearch: { - // hosts: ['localhost:9000'], - // username: 'foo', - // }, - // }, - // ]); - // }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/set_tag.js b/x-pack/test/api_integration/apis/beats/set_tag.js deleted file mode 100644 index 742fe9db251bde..00000000000000 --- a/x-pack/test/api_integration/apis/beats/set_tag.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { ES_INDEX_NAME } from './constants'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const es = getService('legacyEs'); - - describe('set_tag', () => { - it('should create a tag', async () => { - const tagId = 'production'; - await supertest - .put(`/api/beats/tag/${tagId}`) - .set('kbn-xsrf', 'xxx') - .send({ - color: 'green', - }) - .expect(200); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `tag:${tagId}`, - }); - - const tagInEs = esResponse._source; - - expect(tagInEs.type).to.be('tag'); - expect(tagInEs.tag.id).to.be(tagId); - }); - - it('should update an existing tag', async () => { - const tagId = 'production'; - await supertest - .put(`/api/beats/tag/${tagId}`) - .set('kbn-xsrf', 'xxx') - .send({ - color: 'blue', - }) - .expect(200); - - await supertest - .put(`/api/beats/tag/${tagId}`) - .set('kbn-xsrf', 'xxx') - .send({ - color: 'yellow', - }) - .expect(200); - - const esResponse = await es.get({ - index: ES_INDEX_NAME, - id: `tag:${tagId}`, - }); - - const tagInEs = esResponse._source; - - expect(tagInEs.type).to.be('tag'); - expect(tagInEs.tag.id).to.be(tagId); - expect(tagInEs.tag.color).to.be('yellow'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/beats/update_beat.js b/x-pack/test/api_integration/apis/beats/update_beat.js deleted file mode 100644 index 8c67f43839fcc7..00000000000000 --- a/x-pack/test/api_integration/apis/beats/update_beat.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { ES_INDEX_NAME } from './constants'; -import moment from 'moment'; - -export default function ({ getService }) { - const supertest = getService('supertest'); - const randomness = getService('randomness'); - const es = getService('legacyEs'); - const esArchiver = getService('esArchiver'); - - describe('update_beat', () => { - let validEnrollmentToken; - let beat; - const archive = 'beats/list'; - - beforeEach('load beats archive', () => esArchiver.load(archive)); - beforeEach(async () => { - validEnrollmentToken = - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + - 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + - 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI'; - - const version = - randomness.integer({ min: 1, max: 10 }) + - '.' + - randomness.integer({ min: 1, max: 10 }) + - '.' + - randomness.integer({ min: 1, max: 10 }); - - beat = { - type: `${randomness.word()}beat`, - host_name: `www.${randomness.word()}.net`, - name: randomness.word(), - version, - ephemeral_id: randomness.word(), - }; - - await es.index({ - index: ES_INDEX_NAME, - id: `enrollment_token:${validEnrollmentToken}`, - body: { - type: 'enrollment_token', - enrollment_token: { - token: validEnrollmentToken, - expires_on: moment().add(4, 'hours').toJSON(), - }, - }, - }); - }); - - afterEach('unload beats archive', () => esArchiver.unload(archive)); - - it('should update an existing verified beat', async () => { - const beatId = 'foo'; - await supertest - .put(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set( - 'kbn-beats-access-token', - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.' + - 'eyJjcmVhdGVkIjoiMjAxOC0wNi0zMFQwMzo0MjoxNS4yMzBaIiwiaWF0IjoxNTMwMzMwMTM1fQ.' + - 'SSsX2Byyo1B1bGxV8C3G4QldhE5iH87EY_1r21-bwbI' - ) - .send(beat) - .expect(200); - - const beatInEs = await es.get({ - index: ES_INDEX_NAME, - id: `beat:${beatId}`, - }); - - expect(beatInEs._source.beat.id).to.be(beatId); - expect(beatInEs._source.beat.type).to.be(beat.type); - expect(beatInEs._source.beat.host_name).to.be(beat.host_name); - expect(beatInEs._source.beat.version).to.be(beat.version); - expect(beatInEs._source.beat.ephemeral_id).to.be(beat.ephemeral_id); - expect(beatInEs._source.beat.name).to.be(beat.name); - }); - - it('should return an error for an invalid access token', async () => { - const beatId = 'foo'; - const { body } = await supertest - .put(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', randomness.word()) - .send(beat) - .expect(401); - - expect(body.message).to.be('Invalid access token'); - - const beatInEs = await es.get({ - index: ES_INDEX_NAME, - id: `beat:${beatId}`, - }); - - expect(beatInEs._source.beat.id).to.be(beatId); - expect(beatInEs._source.beat.type).to.not.be(beat.type); - expect(beatInEs._source.beat.host_name).to.not.be(beat.host_name); - expect(beatInEs._source.beat.version).to.not.be(beat.version); - expect(beatInEs._source.beat.ephemeral_id).to.not.be(beat.ephemeral_id); - }); - - it('should return an error for a non-existent beat', async () => { - const beatId = randomness.word(); - const { body } = await supertest - .put(`/api/beats/agent/${beatId}`) - .set('kbn-xsrf', 'xxx') - .set('kbn-beats-access-token', validEnrollmentToken) - .send(beat) - .expect(404); - - expect(body.message).to.be('Beat not found'); - }); - }); -} diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index e0328444701777..0d345db58d24f7 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -22,7 +22,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./logstash')); loadTestFile(require.resolve('./kibana')); loadTestFile(require.resolve('./metrics_ui')); - loadTestFile(require.resolve('./beats')); loadTestFile(require.resolve('./console')); loadTestFile(require.resolve('./management')); loadTestFile(require.resolve('./uptime')); diff --git a/x-pack/test/functional/apps/infra/metrics_anomalies.ts b/x-pack/test/functional/apps/infra/metrics_anomalies.ts index 54aaaae256f361..a3160c023d4a7e 100644 --- a/x-pack/test/functional/apps/infra/metrics_anomalies.ts +++ b/x-pack/test/functional/apps/infra/metrics_anomalies.ts @@ -16,7 +16,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const infraSourceConfigurationForm = getService('infraSourceConfigurationForm'); // Failing: See https://github.com/elastic/kibana/issues/100445 - describe.skip('Metrics UI Anomaly Flyout', function () { + describe('Metrics UI Anomaly Flyout', function () { before(async () => { await esArchiver.load('empty_kibana'); }); @@ -62,16 +62,20 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await esArchiver.unload('infra/metrics_anomalies'); }); it('renders the anomaly table with anomalies', async () => { + // default threshold should already be 50 but trying to prevent unknown flakiness by setting it + // https://github.com/elastic/kibana/issues/100445 + await pageObjects.infraHome.goToSettings(); + await pageObjects.infraHome.setAnomaliesThreshold('50'); + await infraSourceConfigurationForm.saveConfiguration(); + await pageObjects.infraHome.goToInventory(); await pageObjects.infraHome.openAnomalyFlyout(); await pageObjects.infraHome.goToAnomaliesTab(); await pageObjects.infraHome.clickHostsAnomaliesDropdown(); await pageObjects.infraHome.setAnomaliesDate('Apr 21, 2021 @ 00:00:00.000'); const hostAnomalies = await pageObjects.infraHome.findAnomalies(); - // expect 2 anomalies with default Anomaly Severity Threshold setting (50) expect(hostAnomalies.length).to.be(2); await pageObjects.infraHome.clickK8sAnomaliesDropdown(); const k8sAnomalies = await pageObjects.infraHome.findAnomalies(); - // expect 3 anomalies with default Anomaly Severity Threshold setting (50) expect(k8sAnomalies.length).to.be(1); await pageObjects.infraHome.closeFlyout(); }); @@ -93,6 +97,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.infraHome.openAnomalyFlyout(); await pageObjects.infraHome.goToAnomaliesTab(); await pageObjects.infraHome.clickHostsAnomaliesDropdown(); + await pageObjects.infraHome.setAnomaliesDate('Apr 21, 2021 @ 00:00:00.000'); const hostAnomalies = await pageObjects.infraHome.findAnomalies(); expect(hostAnomalies.length).to.be(4); await pageObjects.infraHome.clickK8sAnomaliesDropdown(); diff --git a/x-pack/test/functional/apps/management/create_index_pattern_wizard.js b/x-pack/test/functional/apps/management/create_index_pattern_wizard.js index 149c0fd09c0972..246256cb4c2a1d 100644 --- a/x-pack/test/functional/apps/management/create_index_pattern_wizard.js +++ b/x-pack/test/functional/apps/management/create_index_pattern_wizard.js @@ -7,7 +7,7 @@ export default function ({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); - const es = getService('legacyEs'); + const es = getService('es'); const PageObjects = getPageObjects(['settings', 'common']); describe('"Create Index Pattern" wizard', function () { diff --git a/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js b/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js new file mode 100644 index 00000000000000..efe02d2d85156a --- /dev/null +++ b/x-pack/test/functional/apps/maps/embeddable/filter_by_map_extent.js @@ -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. + */ + +export default function ({ getPageObjects, getService }) { + const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'lens', 'maps']); + + const testSubjects = getService('testSubjects'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const security = getService('security'); + + describe('filter by map extent', () => { + before(async () => { + await security.testUser.setRoles( + ['test_logstash_reader', 'global_maps_all', 'global_dashboard_all'], + false + ); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.gotoDashboardEditMode('filter by map extent dashboard'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.dashboard.waitForRenderComplete(); + }); + + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not filter dashboard by map extent before "filter by map extent" is enabled', async () => { + await PageObjects.lens.assertMetric('Count of records', '6'); + }); + + it('should filter dashboard by map extent when "filter by map extent" is enabled', async () => { + const mapPanelHeader = await dashboardPanelActions.getPanelHeading('document example'); + await dashboardPanelActions.openContextMenuMorePanel(mapPanelHeader); + await await testSubjects.click('embeddablePanelAction-FILTER_BY_MAP_EXTENT'); + await PageObjects.header.waitUntilLoadingHasFinished(); + + await PageObjects.lens.assertMetric('Count of records', '1'); + }); + + it('should filter dashboard by new map extent when map is moved', async () => { + await PageObjects.maps.setView(32.95539, -93.93054, 5); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Count of records', '2'); + }); + + it('should remove map extent filter dashboard when "filter by map extent" is disabled', async () => { + const mapPanelHeader = await dashboardPanelActions.getPanelHeading('document example'); + await dashboardPanelActions.openContextMenuMorePanel(mapPanelHeader); + await await testSubjects.click('embeddablePanelAction-FILTER_BY_MAP_EXTENT'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.lens.assertMetric('Count of records', '6'); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/embeddable/index.js b/x-pack/test/functional/apps/maps/embeddable/index.js index 552f830e2a379a..da5d4b8945da77 100644 --- a/x-pack/test/functional/apps/maps/embeddable/index.js +++ b/x-pack/test/functional/apps/maps/embeddable/index.js @@ -13,5 +13,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./embeddable_library')); loadTestFile(require.resolve('./embeddable_state')); loadTestFile(require.resolve('./tooltip_filter_actions')); + loadTestFile(require.resolve('./filter_by_map_extent')); }); } diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts index 302a33d947259c..bc2f3277206907 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts @@ -613,7 +613,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts index c713343b3e3804..693b8b5a403f11 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts @@ -74,6 +74,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.securityUI.loginAsMlPowerUser(); }); + after(async () => { + await ml.testResources.deleteMLTestDashboard(); + }); + for (const testData of testDataList) { describe(testData.suiteSuffix, function () { before(async () => { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index 611c7be8b0677d..85eeacc58514e2 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -278,7 +278,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts new file mode 100644 index 00000000000000..a743e00b64addf --- /dev/null +++ b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import { + TimeRangeType, + TIME_RANGE_TYPE, +} from '../../../../../plugins/ml/public/application/jobs/components/custom_url_editor/constants'; + +interface DiscoverUrlConfig { + label: string; + indexPattern: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; +} + +interface DashboardUrlConfig { + label: string; + dashboardName: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; +} + +interface OtherUrlConfig { + label: string; + url: string; +} + +// @ts-expect-error doesn't implement the full interface +const JOB_CONFIG: Job = { + job_id: `fq_multi_1_custom_urls`, + description: 'mean(responsetime) partition=airline on farequote dataset with 30m bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '30m', + influencers: ['airline'], + detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: true }, +}; + +// @ts-expect-error doesn't implement the full interface +const DATAFEED_CONFIG: Datafeed = { + datafeed_id: 'datafeed-fq_multi_1_custom_urls', + indices: ['ft_farequote'], + job_id: 'fq_multi_1_custom_urls', + query: { bool: { must: [{ match_all: {} }] } }, +}; + +const testDiscoverCustomUrl: DiscoverUrlConfig = { + label: 'Show data', + indexPattern: 'ft_farequote', + queryEntityFieldNames: ['airline'], + timeRange: TIME_RANGE_TYPE.AUTO, +}; + +const testDashboardCustomUrl: DashboardUrlConfig = { + label: 'Show dashboard', + dashboardName: 'ML Test', + queryEntityFieldNames: [], + timeRange: TIME_RANGE_TYPE.INTERVAL, + timeRangeInterval: '1h', +}; + +const testOtherCustomUrl: OtherUrlConfig = { + label: 'elastic.co', + url: 'https://www.elastic.co/', +}; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const browser = getService('browser'); + + describe('custom urls', function () { + this.tags(['mlqa']); + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createMLTestDashboardIfNeeded(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.testResources.deleteMLTestDashboard(); + await ml.api.cleanMlIndices(); + }); + + it('opens the custom URLs tab in the edit job flyout', async () => { + await ml.testExecution.logTestStep('load the job management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.testExecution.logTestStep('open the custom URLs tab in the edit job flyout'); + await ml.jobTable.openEditCustomUrlsForJobTab(JOB_CONFIG.job_id); + await ml.jobTable.closeEditJobFlyout(); + }); + + it('adds a custom URL with query entities to Discover in the edit job flyout', async () => { + await ml.jobTable.addDiscoverCustomUrl(JOB_CONFIG.job_id, testDiscoverCustomUrl); + }); + + it('adds a custom URL to Dashboard in the edit job flyout', async () => { + await ml.jobTable.addDashboardCustomUrl(JOB_CONFIG.job_id, testDashboardCustomUrl); + }); + + it('adds a custom URL to an external page in the edit job flyout', async () => { + await ml.jobTable.addOtherTypeCustomUrl(JOB_CONFIG.job_id, testOtherCustomUrl); + }); + + it('tests other type custom URL', async () => { + await ml.jobTable.testOtherTypeCustomUrlAction(JOB_CONFIG.job_id, 2, testOtherCustomUrl.url); + }); + + it('edits other type custom URL', async () => { + const edit = { + label: `${testOtherCustomUrl.url} edited`, + url: `${testOtherCustomUrl.url}guide/index.html`, + }; + await ml.testExecution.logTestStep('edit the custom URL in the edit job flyout'); + await ml.jobTable.editCustomUrl(JOB_CONFIG.job_id, 2, edit); + + await ml.testExecution.logTestStep('tests custom URL edit has been applied'); + await ml.jobTable.testOtherTypeCustomUrlAction(JOB_CONFIG.job_id, 2, edit.url); + await ml.jobTable.closeEditJobFlyout(); + }); + + it('deletes a custom URL', async () => { + await ml.jobTable.deleteCustomUrl(JOB_CONFIG.job_id, 2); + }); + + // wrapping into own describe to make sure new tab is cleaned up even if test failed + // see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122 + describe('tests Discover type custom URL', () => { + let tabsCount = 1; + const docCountFormatted = '268'; + + it('opens Discover page from test link in the edit job flyout', async () => { + await ml.jobTable.openTestCustomUrl(JOB_CONFIG.job_id, 0); + await browser.switchTab(1); + tabsCount++; + await ml.jobTable.testDiscoverCustomUrlAction(docCountFormatted); + }); + + after(async () => { + if (tabsCount > 1) { + await browser.closeCurrentWindow(); + await browser.switchTab(0); + await ml.jobTable.closeEditJobFlyout(); + } + }); + }); + + // wrapping into own describe to make sure new tab is cleaned up even if test failed + // see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122 + describe('tests Dashboard type custom URL', () => { + let tabsCount = 1; + const testDashboardPanelCount = 0; // ML Test dashboard has no content. + + it('opens Dashboard page from test link in the edit job flyout', async () => { + await ml.jobTable.openTestCustomUrl(JOB_CONFIG.job_id, 1); + await browser.switchTab(1); + tabsCount++; + await ml.jobTable.testDashboardCustomUrlAction(testDashboardPanelCount); + }); + + after(async () => { + if (tabsCount > 1) { + await browser.closeCurrentWindow(); + await browser.switchTab(0); + await ml.jobTable.closeEditJobFlyout(); + } + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts index 6b7afacbb721a6..d87da8469db118 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./date_nanos_job')); loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./aggregated_scripted_job')); + loadTestFile(require.resolve('./custom_urls')); }); } diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index b12eff71d8258b..256f9da313e4ed 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -299,7 +299,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts index 8fea197e056674..2bdda2c81c71d2 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts @@ -336,7 +336,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index 4f9b265c3b1ed6..eedb130215f7fa 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -262,7 +262,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts b/x-pack/test/functional/apps/reporting/index.ts similarity index 53% rename from x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts rename to x-pack/test/functional/apps/reporting/index.ts index c5f871884609ae..286693f01ac52d 100644 --- a/x-pack/plugins/beats_management/public/lib/adapters/tokens/memory_tokens_adapter.ts +++ b/x-pack/test/functional/apps/reporting/index.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { CMTokensAdapter } from './adapter_types'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export class MemoryTokensAdapter implements CMTokensAdapter { - public async createEnrollmentTokens(): Promise { - return ['2jnwkrhkwuehriauhweair']; - } +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Reporting', function () { + loadTestFile(require.resolve('./reporting')); + }); } diff --git a/x-pack/test/functional/apps/reporting/reporting.ts b/x-pack/test/functional/apps/reporting/reporting.ts new file mode 100644 index 00000000000000..a9d089e9fd3978 --- /dev/null +++ b/x-pack/test/functional/apps/reporting/reporting.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const pageObjects = getPageObjects(['dashboard', 'common', 'reporting']); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('Reporting', function () { + this.tags(['smoke', 'ciGroup2']); + before(async () => { + await esArchiver.loadIfNeeded('packaging'); + }); + + after(async () => { + await esArchiver.unload('packaging'); + await es.deleteByQuery({ + index: '.reporting-*', + refresh: true, + body: { query: { match_all: {} } }, + }); + }); + + it('downloaded PDF has OK status', async function () { + this.timeout(180000); + + await pageObjects.common.navigateToApp('dashboards'); + await pageObjects.dashboard.loadSavedDashboard('dashboard'); + await pageObjects.reporting.openPdfReportingPanel(); + await pageObjects.reporting.clickGenerateReportButton(); + + const url = await pageObjects.reporting.getReportURL(60000); + const res = await pageObjects.reporting.getResponse(url); + + expect(res.status).to.equal(200); + expect(res.get('content-type')).to.equal('application/pdf'); + }); + }); +} diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index e8a67f8899a36f..4e4d4974ced09d 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -14,8 +14,7 @@ export default function enterSpaceFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'spaceSelector']); - // FLAKY: https://github.com/elastic/kibana/issues/100012 - describe.skip('Enter Space', function () { + describe('Enter Space', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('spaces/enter_space'); diff --git a/x-pack/test/functional/apps/spaces/spaces_selection.ts b/x-pack/test/functional/apps/spaces/spaces_selection.ts index f3d3665bf9f613..99efdf29eecb97 100644 --- a/x-pack/test/functional/apps/spaces/spaces_selection.ts +++ b/x-pack/test/functional/apps/spaces/spaces_selection.ts @@ -22,8 +22,7 @@ export default function spaceSelectorFunctionalTests({ 'spaceSelector', ]); - // FLAKY: https://github.com/elastic/kibana/issues/99581 - describe.skip('Spaces', function () { + describe('Spaces', function () { this.tags('includeFirefox'); describe('Space Selector', () => { before(async () => { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 573350dad24d0f..ee5be48a07663c 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -59,6 +59,7 @@ export default async function ({ readConfigFile }) { resolve(__dirname, './apps/transform'), resolve(__dirname, './apps/reporting_management'), resolve(__dirname, './apps/management'), + resolve(__dirname, './apps/reporting'), // This license_management file must be last because it is destructive. resolve(__dirname, './apps/license_management'), diff --git a/x-pack/test/functional/es_archives/maps/kibana/data.json b/x-pack/test/functional/es_archives/maps/kibana/data.json index 631efb58f9c7bb..4a879c20f19ab8 100644 --- a/x-pack/test/functional/es_archives/maps/kibana/data.json +++ b/x-pack/test/functional/es_archives/maps/kibana/data.json @@ -1149,6 +1149,56 @@ } } +{ + "type": "doc", + "value": { + "id": "dashboard:42f6f040-b34f-11eb-8c95-dd19591c63df", + "index": ".kibana", + "source": { + "dashboard": { + "title" : "filter by map extent dashboard", + "hits" : 0, + "description" : "", + "panelsJSON" : "[{\"version\":\"8.0.0\",\"type\":\"map\",\"gridData\":{\"x\":0,\"y\":0,\"w\":29,\"h\":21,\"i\":\"24ade730-afe4-42b6-919a-c4e0a98c94f2\"},\"panelIndex\":\"24ade730-afe4-42b6-919a-c4e0a98c94f2\",\"embeddableConfig\":{\"mapCenter\":{\"lat\":38.64679,\"lon\":-120.96481,\"zoom\":7.06},\"mapBuffer\":{\"minLon\":-125.44180499999999,\"minLat\":36.364824999999996,\"maxLon\":-116.603825,\"maxLat\":40.943405},\"isLayerTOCOpen\":true,\"openTOCDetails\":[],\"hiddenLayers\":[],\"enhancements\":{}},\"panelRefName\":\"panel_24ade730-afe4-42b6-919a-c4e0a98c94f2\"},{\"version\":\"8.0.0\",\"type\":\"lens\",\"gridData\":{\"x\":29,\"y\":0,\"w\":10,\"h\":21,\"i\":\"44eb3c47-f6ad-4da8-993b-13c10997d585\"},\"panelIndex\":\"44eb3c47-f6ad-4da8-993b-13c10997d585\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"3cda3519-055a-4b9c-8759-caa28388298c\":{\"columns\":{\"26acba84-22ca-4625-b2ac-5309945e9b30\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"26acba84-22ca-4625-b2ac-5309945e9b30\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"layerId\":\"3cda3519-055a-4b9c-8759-caa28388298c\",\"accessor\":\"26acba84-22ca-4625-b2ac-5309945e9b30\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"c698b940-e149-11e8-a35a-370a8516603a\",\"name\":\"indexpattern-datasource-layer-3cda3519-055a-4b9c-8759-caa28388298c\"}]},\"enhancements\":{},\"hidePanelTitles\":false},\"title\":\"Count panel\"}]", + "optionsJSON" : "{\"hidePanelTitles\":false,\"useMargins\":true}", + "version" : 1, + "timeRestore" : true, + "timeTo" : "2015-09-20T01:00:00.000Z", + "timeFrom" : "2015-09-20T00:00:00.000Z", + "refreshInterval" : { + "pause" : true, + "value" : 1000 + }, + "kibanaSavedObjectMeta" : { + "searchSourceJSON" : "{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}" + } + }, + "type" : "dashboard", + "references" : [ + { + "name" : "24ade730-afe4-42b6-919a-c4e0a98c94f2:panel_24ade730-afe4-42b6-919a-c4e0a98c94f2", + "type" : "map", + "id" : "d2e73f40-e14a-11e8-a35a-370a8516603a" + }, + { + "type" : "index-pattern", + "id" : "c698b940-e149-11e8-a35a-370a8516603a", + "name" : "44eb3c47-f6ad-4da8-993b-13c10997d585:indexpattern-datasource-current-indexpattern" + }, + { + "type" : "index-pattern", + "id" : "c698b940-e149-11e8-a35a-370a8516603a", + "name" : "44eb3c47-f6ad-4da8-993b-13c10997d585:indexpattern-datasource-layer-3cda3519-055a-4b9c-8759-caa28388298c" + } + ], + "migrationVersion" : { + "dashboard" : "7.11.0" + }, + "updated_at" : "2021-05-12T18:24:17.228Z" + } + } +} + { "type": "doc", "value": { diff --git a/x-pack/test/functional/es_archives/packaging/data.json.gz b/x-pack/test/functional/es_archives/packaging/data.json.gz new file mode 100644 index 00000000000000..69c9e4cb4d8a22 Binary files /dev/null and b/x-pack/test/functional/es_archives/packaging/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/packaging/mappings.json b/x-pack/test/functional/es_archives/packaging/mappings.json new file mode 100644 index 00000000000000..182d281c9a3ded --- /dev/null +++ b/x-pack/test/functional/es_archives/packaging/mappings.json @@ -0,0 +1,2561 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "0359d7fcc04da9878ee9aadbda38ba55", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "background-session": "721df406dbb7e35ac22e4df6c3ad2b2a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "8a50736330e953bca91747723a319593", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "f74dfe498e1849267cda41580b2be110", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "executionStatus": { + "properties": { + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } + }, + "lastExecutionDate": { + "type": "date" + }, + "status": { + "type": "keyword" + } + } + }, + "meta": { + "properties": { + "versionApiKeyLastmodified": { + "type": "keyword" + } + } + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedAt": { + "type": "date" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "api_key_pending_invalidation": { + "properties": { + "apiKeyId": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "background-session": { + "properties": { + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "status": { + "type": "keyword" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "alertId": { + "type": "keyword" + }, + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "index": { + "type": "keyword" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "enterprise_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "install_source": { + "type": "keyword" + }, + "install_started_at": { + "type": "date" + }, + "install_status": { + "type": "keyword" + }, + "install_version": { + "type": "keyword" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "ack_data": { + "type": "text" + }, + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "policy_id": { + "type": "keyword" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "upgrade_started_at": { + "type": "date" + }, + "upgraded_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "dynamic": "false", + "type": "object" + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_policies": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "config_yaml": { + "type": "text" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "policy_id": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_urls": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "dynamic": "false", + "type": "object" + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "dashboard": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "lens": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-job": { + "properties": { + "datafeed_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "job_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "indexNames": { + "type": "text" + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "tag": { + "properties": { + "color": { + "type": "text" + }, + "description": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "dynamic": "false", + "type": "object" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + }, + "index": "foo", + "mappings": { + "properties": { + "field": { + "type": "long" + } + } + }, + "settings": { + "index": { + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/page_objects/gis_page.ts b/x-pack/test/functional/page_objects/gis_page.ts index 4a898967419b6f..99d7172651a4d1 100644 --- a/x-pack/test/functional/page_objects/gis_page.ts +++ b/x-pack/test/functional/page_objects/gis_page.ts @@ -21,12 +21,12 @@ export function GisPageProvider({ getService, getPageObjects }: FtrProviderConte const comboBox = getService('comboBox'); const renderable = getService('renderable'); const browser = getService('browser'); - const MenuToggle = getService('MenuToggle'); + const menuToggle = getService('menuToggle'); const listingTable = getService('listingTable'); const monacoEditor = getService('monacoEditor'); const dashboardPanelActions = getService('dashboardPanelActions'); - const setViewPopoverToggle = new MenuToggle({ + const setViewPopoverToggle = menuToggle.create({ name: 'SetView Popover', menuTestSubject: 'mapSetViewForm', toggleButtonTestSubject: 'toggleSetViewVisibilityButton', diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index b7288d5927b4c9..b61bc8871482ff 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -123,6 +123,25 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte await this.assertRadioGroupValue(testSubject, value); }, + async assertSelectSelectedOptionVisibleText(testSubject: string, visibleText: string) { + // Need to validate the selected option text, as the option value may be different to the visible text. + const selectControl = await testSubjects.find(testSubject); + const selectedValue = await selectControl.getAttribute('value'); + const selectedOption = await selectControl.findByCssSelector(`[value="${selectedValue}"]`); + const selectedOptionText = await selectedOption.getVisibleText(); + expect(selectedOptionText).to.eql( + visibleText, + `Expected selected option visible text to be '${visibleText}' (got '${selectedOptionText}')` + ); + }, + + async selectSelectValueByVisibleText(testSubject: string, visibleText: string) { + // Cannot use await testSubjects.selectValue as the option value may be different to the text. + const selectControl = await testSubjects.find(testSubject); + await selectControl.type(visibleText); + await this.assertSelectSelectedOptionVisibleText(testSubject, visibleText); + }, + async setMultiSelectFilter(testDataSubj: string, fieldTypes: string[]) { await testSubjects.clickWhenNotDisabled(`${testDataSubj}-button`); await testSubjects.existOrFail(`${testDataSubj}-popover`); diff --git a/x-pack/test/functional/services/ml/custom_urls.ts b/x-pack/test/functional/services/ml/custom_urls.ts index 0b24c565b2fa8e..67640eff7129e0 100644 --- a/x-pack/test/functional/services/ml/custom_urls.ts +++ b/x-pack/test/functional/services/ml/custom_urls.ts @@ -12,10 +12,25 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export type MlCustomUrls = ProvidedType; -export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderContext) { +export function MachineLearningCustomUrlsProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); + const PageObjects = getPageObjects(['dashboard', 'discover', 'header']); return { + async assertCustomUrlsLength(expectedLength: number) { + const customUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + const actualLength = customUrls.length; + expect(expectedLength).to.eql( + actualLength, + `Expected number of custom urls to be '${expectedLength}' (got '${actualLength}')` + ); + }, + async assertCustomUrlLabelValue(expectedValue: string) { const actualCustomUrlLabel = await testSubjects.getAttribute( 'mlJobCustomUrlLabelInput', @@ -27,15 +42,68 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon ); }, - async setCustomUrlLabel(customUrlsLabel: string) { - await testSubjects.setValue('mlJobCustomUrlLabelInput', customUrlsLabel, { + async setCustomUrlLabel(customUrlLabel: string) { + await testSubjects.setValue('mlJobCustomUrlLabelInput', customUrlLabel, { clearWithKeyboard: true, }); - await this.assertCustomUrlLabelValue(customUrlsLabel); + await this.assertCustomUrlLabelValue(customUrlLabel); }, - async assertCustomUrlItem(index: number, expectedLabel: string) { - await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`); + async assertCustomUrlQueryEntitySelection(expectedFieldNames: string[]) { + const actualFieldNames = await comboBox.getComboBoxSelectedOptions( + 'mlJobCustomUrlQueryEntitiesInput > comboBoxInput' + ); + expect(actualFieldNames).to.eql( + expectedFieldNames, + `Expected query entity selection to be '${expectedFieldNames}' (got '${actualFieldNames}')` + ); + }, + + async setCustomUrlQueryEntityFieldNames(fieldNames: string[]) { + for (const fieldName of fieldNames) { + await comboBox.set('mlJobCustomUrlQueryEntitiesInput > comboBoxInput', fieldName); + } + await this.assertCustomUrlQueryEntitySelection(fieldNames); + }, + + async assertCustomUrlTimeRangeIntervalValue(expectedInterval: string) { + const actualCustomUrlTimeRangeInterval = await testSubjects.getAttribute( + 'mlJobCustomUrlTimeRangeIntervalInput', + 'value' + ); + expect(actualCustomUrlTimeRangeInterval).to.eql( + expectedInterval, + `Expected custom url time range interval to be '${expectedInterval}' (got '${actualCustomUrlTimeRangeInterval}')` + ); + }, + + async setCustomUrlTimeRangeInterval(interval: string) { + await testSubjects.setValue('mlJobCustomUrlTimeRangeIntervalInput', interval, { + clearWithKeyboard: true, + }); + await this.assertCustomUrlTimeRangeIntervalValue(interval); + }, + + async assertCustomUrlOtherTypeUrlValue(expectedUrl: string) { + const actualCustomUrlValue = await testSubjects.getAttribute( + 'mlJobCustomUrlOtherTypeUrlInput', + 'value' + ); + expect(actualCustomUrlValue).to.eql( + expectedUrl, + `Expected other type custom url value to be '${expectedUrl}' (got '${actualCustomUrlValue}')` + ); + }, + + async setCustomUrlOtherTypeUrl(url: string) { + await testSubjects.setValue('mlJobCustomUrlOtherTypeUrlInput', url, { + clearWithKeyboard: true, + }); + await this.assertCustomUrlOtherTypeUrlValue(url); + }, + + async assertCustomUrlLabel(index: number, expectedLabel: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlLabelInput_${index}`); const actualLabel = await testSubjects.getAttribute( `mlJobEditCustomUrlLabelInput_${index}`, 'value' @@ -46,6 +114,44 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon ); }, + async assertCustomUrlUrlValue(index: number, expectedUrl: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlInput_${index}`); + const actualUrl = await testSubjects.getAttribute( + `mlJobEditCustomUrlInput_${index}`, + 'value' + ); + expect(actualUrl).to.eql( + expectedUrl, + `Expected custom url item to be '${expectedUrl}' (got '${actualUrl}')` + ); + }, + + async editCustomUrlLabel(index: number, label: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlLabelInput_${index}`); + await testSubjects.setValue(`mlJobEditCustomUrlLabelInput_${index}`, label, { + clearWithKeyboard: true, + }); + await this.assertCustomUrlLabel(index, label); + }, + + async editCustomUrlUrlValue(index: number, urlValue: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlInput_${index}`); + await testSubjects.setValue(`mlJobEditCustomUrlInput_${index}`, urlValue, { + clearWithKeyboard: true, + }); + + // Click away, so the textarea reverts back to the standard input. + await testSubjects.click(`mlJobEditCustomUrlLabelInput_${index}`); + await this.assertCustomUrlUrlValue(index, urlValue); + }, + + async deleteCustomUrl(index: number) { + await testSubjects.existOrFail(`mlJobEditDeleteCustomUrlButton_${index}`); + const beforeCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + await testSubjects.click(`mlJobEditDeleteCustomUrlButton_${index}`); + await this.assertCustomUrlsLength(beforeCustomUrls.length - 1); + }, + /** * Submits the custom url form and adds it to the list. * @param formContainerSelector - selector for the element that wraps the custom url creation form. @@ -54,5 +160,33 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon await testSubjects.click('mlJobAddCustomUrl'); await testSubjects.missingOrFail(formContainerSelector, { timeout: 10 * 1000 }); }, + + async clickTestCustomUrl(index: number) { + await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`); + await testSubjects.click(`mlJobEditCustomUrlItem_${index} > mlJobEditTestCustomUrlButton`); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + + async assertDiscoverCustomUrlAction(expectedHitCountFormatted: string) { + await PageObjects.discover.waitForDiscoverAppOnScreen(); + await retry.tryForTime(5000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.eql( + expectedHitCountFormatted, + `Expected Discover hit count to be '${expectedHitCountFormatted}' (got '${hitCount}')` + ); + }); + }, + + async assertDashboardCustomUrlAction(expectedPanelCount: number) { + await PageObjects.dashboard.waitForRenderComplete(); + await retry.tryForTime(5000, async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql( + expectedPanelCount, + `Expected Dashboard panel count to be '${expectedPanelCount}' (got '${panelCount}')` + ); + }); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 6a2e1158e70a37..64298bbdedd63c 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -90,7 +90,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSelection = MachineLearningJobSelectionProvider(context); const jobSourceSelection = MachineLearningJobSourceSelectionProvider(context); - const jobTable = MachineLearningJobTableProvider(context); + const jobTable = MachineLearningJobTableProvider(context, commonUI, customUrls); const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context); const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context, commonUI); const jobWizardCategorization = MachineLearningJobWizardCategorizationProvider(context); diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index a179983a4627f6..a39e62d6281fe3 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -8,8 +8,20 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; - -export function MachineLearningJobTableProvider({ getService }: FtrProviderContext) { +import { MlCommonUI } from './common_ui'; +import { MlCustomUrls } from './custom_urls'; + +import { + TimeRangeType, + TIME_RANGE_TYPE, + URL_TYPE, +} from '../../../../plugins/ml/public/application/jobs/components/custom_url_editor/constants'; + +export function MachineLearningJobTableProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI, + customUrls: MlCustomUrls +) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -311,6 +323,12 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte await testSubjects.existOrFail('~mlPageJobWizard'); } + public async clickEditJobAction(jobId: string) { + await this.ensureJobActionsMenuOpen(jobId); + await testSubjects.click('mlActionButtonEditJob'); + await testSubjects.existOrFail('mlJobEditFlyout'); + } + public async clickDeleteJobAction(jobId: string) { await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonDeleteJob'); @@ -456,5 +474,189 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } }); } + + public async openEditCustomUrlsForJobTab(jobId: string) { + await this.clickEditJobAction(jobId); + // click Custom URLs tab + await testSubjects.click('mlEditJobFlyout-customUrls'); + await this.ensureEditCustomUrlTabOpen(); + } + + public async ensureEditCustomUrlTabOpen() { + await testSubjects.existOrFail('mlJobOpenCustomUrlFormButton', { timeout: 5000 }); + } + + public async closeEditJobFlyout() { + if (await testSubjects.exists('mlEditJobFlyoutCloseButton')) { + await testSubjects.click('mlEditJobFlyoutCloseButton'); + await testSubjects.missingOrFail('mlJobEditFlyout'); + } + } + + public async saveEditJobFlyoutChanges() { + await testSubjects.click('mlEditJobFlyoutSaveButton'); + await testSubjects.missingOrFail('mlJobEditFlyout', { timeout: 5000 }); + } + + public async clickOpenCustomUrlEditor() { + await this.ensureEditCustomUrlTabOpen(); + await testSubjects.click('mlJobOpenCustomUrlFormButton'); + await testSubjects.existOrFail('mlJobCustomUrlForm'); + } + + public async addDiscoverCustomUrl( + jobId: string, + customUrl: { + label: string; + indexPattern: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; + } + ) { + await this.openEditCustomUrlsForJobTab(jobId); + + const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + + // Fill-in the form + await this.clickOpenCustomUrlEditor(); + await customUrls.setCustomUrlLabel(customUrl.label); + await mlCommonUI.selectRadioGroupValue( + `mlJobCustomUrlLinkToTypeInput`, + URL_TYPE.KIBANA_DISCOVER + ); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlDiscoverIndexPatternInput', + customUrl.indexPattern + ); + await customUrls.setCustomUrlQueryEntityFieldNames(customUrl.queryEntityFieldNames); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlTimeRangeInput', + customUrl.timeRange + ); + if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) { + await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!); + } + + // Save custom URL + await testSubjects.click('mlJobAddCustomUrl'); + const expectedIndex = existingCustomUrls.length; + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + + // Save the job + await this.saveEditJobFlyoutChanges(); + } + + public async addDashboardCustomUrl( + jobId: string, + customUrl: { + label: string; + dashboardName: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; + } + ) { + await this.openEditCustomUrlsForJobTab(jobId); + + const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + + // Fill-in the form + await this.clickOpenCustomUrlEditor(); + await customUrls.setCustomUrlLabel(customUrl.label); + await mlCommonUI.selectRadioGroupValue( + `mlJobCustomUrlLinkToTypeInput`, + URL_TYPE.KIBANA_DASHBOARD + ); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlDashboardNameInput', + customUrl.dashboardName + ); + await customUrls.setCustomUrlQueryEntityFieldNames(customUrl.queryEntityFieldNames); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlTimeRangeInput', + customUrl.timeRange + ); + if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) { + await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!); + } + + // Save custom URL + await testSubjects.click('mlJobAddCustomUrl'); + const expectedIndex = existingCustomUrls.length; + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + + // Save the job + await this.saveEditJobFlyoutChanges(); + } + + public async addOtherTypeCustomUrl(jobId: string, customUrl: { label: string; url: string }) { + await this.openEditCustomUrlsForJobTab(jobId); + + const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + + // Fill-in the form + await this.clickOpenCustomUrlEditor(); + await customUrls.setCustomUrlLabel(customUrl.label); + await mlCommonUI.selectRadioGroupValue(`mlJobCustomUrlLinkToTypeInput`, URL_TYPE.OTHER); + await customUrls.setCustomUrlOtherTypeUrl(customUrl.url); + + // Save custom URL + await testSubjects.click('mlJobAddCustomUrl'); + const expectedIndex = existingCustomUrls.length; + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + + // Save the job + await this.saveEditJobFlyoutChanges(); + } + + public async editCustomUrl( + jobId: string, + indexInList: number, + customUrl: { label: string; url: string } + ) { + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.editCustomUrlLabel(indexInList, customUrl.label); + await customUrls.editCustomUrlUrlValue(indexInList, customUrl.url); + + // Save the edit + await this.saveEditJobFlyoutChanges(); + } + + public async deleteCustomUrl(jobId: string, indexInList: number) { + await this.openEditCustomUrlsForJobTab(jobId); + const beforeCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + await customUrls.deleteCustomUrl(indexInList); + + // Save the edit and check the custom URL has been deleted. + await testSubjects.click('mlEditJobFlyoutSaveButton'); + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.assertCustomUrlsLength(beforeCustomUrls.length - 1); + await this.closeEditJobFlyout(); + } + + public async openTestCustomUrl(jobId: string, indexInList: number) { + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.clickTestCustomUrl(indexInList); + } + + public async testDiscoverCustomUrlAction(expectedHitCountFormatted: string) { + await customUrls.assertDiscoverCustomUrlAction(expectedHitCountFormatted); + } + + public async testDashboardCustomUrlAction(expectedPanelCount: number) { + await customUrls.assertDashboardCustomUrlAction(expectedPanelCount); + } + + public async testOtherTypeCustomUrlAction( + jobId: string, + indexInList: number, + expectedUrl: string + ) { + // Can't test the contents of the external page, so just check the expected URL. + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.assertCustomUrlUrlValue(indexInList, expectedUrl); + await this.closeEditJobFlyout(); + } })(); } diff --git a/x-pack/test/functional/services/ml/job_wizard_common.ts b/x-pack/test/functional/services/ml/job_wizard_common.ts index 7754432c99aba9..2990f700597671 100644 --- a/x-pack/test/functional/services/ml/job_wizard_common.ts +++ b/x-pack/test/functional/services/ml/job_wizard_common.ts @@ -527,7 +527,7 @@ export function MachineLearningJobWizardCommonProvider( const expectedIndex = existingCustomUrls.length; - await customUrls.assertCustomUrlItem(expectedIndex, customUrl.label); + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); }, async ensureAdvancedSectionOpen() { diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index f967099e10fa36..a8db7ccb7a7648 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -305,6 +305,10 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider await this.createDashboardIfNeeded(dashboards.mlTestDashboard); }, + async deleteMLTestDashboard() { + await this.deleteDashboardByTitle(dashboards.mlTestDashboard.requestBody.attributes.title); + }, + async createDashboardIfNeeded(dashboard: any) { const title = dashboard.requestBody.attributes.title; const dashboardId = await this.getDashboardId(title); diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index d51cc3f9f9938f..10a81e43090886 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -95,7 +95,9 @@ export class AlertingFixturePlugin implements Plugin { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/100697 + describe.skip('Resolver tests for the entity route', () => { describe('winlogbeat tests', () => { before(async () => { await esArchiver.load('endpoint/resolver/winlogbeat'); diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 63a9f8998a99db..7c95a53b75ee78 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -41,7 +41,6 @@ { "path": "../plugins/alerting/tsconfig.json" }, { "path": "../plugins/apm/tsconfig.json" }, { "path": "../plugins/banners/tsconfig.json" }, - { "path": "../plugins/beats_management/tsconfig.json" }, { "path": "../plugins/cases/tsconfig.json" }, { "path": "../plugins/cloud/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, diff --git a/yarn.lock b/yarn.lock index 9967cedea9fde1..a92dadf08dde74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1366,10 +1366,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@29.1.0": - version "29.1.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-29.1.0.tgz#2850aa30d5e00aa8a1ab4974ea36f3c960a8e457" - integrity sha512-/nHT8niLtvSwX3dyEeIQWXEEZrB3xgjLIdlnqZhQXEdHqDQnxlehOMsTqWWws7jS/5uRq/sg+8N2z1xEb+odDw== +"@elastic/charts@29.2.0": + version "29.2.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-29.2.0.tgz#cd65887c0ca1d420493aee1b570c862ca0df5311" + integrity sha512-gj3Gew9zy8XPNEkAAznOjncO5AF63jy/X1k1VIcNPqdqMi07YYCZwCQjMzUVoS4RN6X4GSzxhrYfGAeyZP8gqg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -2682,7 +2682,7 @@ version "0.0.0" uid "" -"@kbn/monaco@link:packages/kbn-monaco": +"@kbn/monaco@link:bazel-bin/packages/kbn-monaco/npm_module": version "0.0.0" uid "" @@ -2854,32 +2854,32 @@ resolved "https://registry.yarnpkg.com/@mapbox/extent/-/extent-0.4.0.tgz#3e591f32e1f0c3981c864239f7b0ac06e610f8a9" integrity sha1-PlkfMuHww5gchkI597CsBuYQ+Kk= -"@mapbox/geojson-area@^0.2.1": +"@mapbox/geojson-area@^0.2.2": version "0.2.2" resolved "https://registry.yarnpkg.com/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz#18d7814aa36bf23fbbcc379f8e26a22927debf10" integrity sha1-GNeBSqNr8j+7zDefjiaiKSfevxA= dependencies: wgs84 "0.0.0" -"@mapbox/geojson-coords@0.0.0": - version "0.0.0" - resolved "https://registry.yarnpkg.com/@mapbox/geojson-coords/-/geojson-coords-0.0.0.tgz#4847a5b96059666e527a2139e75e35d84fd58f50" - integrity sha1-SEeluWBZZm5SeiE551412E/Vj1A= +"@mapbox/geojson-coords@0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-coords/-/geojson-coords-0.0.1.tgz#31338ac5bc7b2e663409fb129643257ce715594f" + integrity sha512-cdMlqmDl1vzAl2E0XC2zIuqM74vdet0Dq2el49haJEVbGpC8se40j5UcsnBK/gsvZzrume30fon1u/aSYMXG4Q== dependencies: "@mapbox/geojson-normalize" "0.0.1" - geojson-flatten "~0.2.1" + geojson-flatten "^1.0.4" -"@mapbox/geojson-extent@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@mapbox/geojson-extent/-/geojson-extent-0.3.2.tgz#a1bdb2015afd0e031c18c3f29f7eb229e4e1950f" - integrity sha1-ob2yAVr9DgMcGMPyn36yKeThlQ8= +"@mapbox/geojson-extent@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@mapbox/geojson-extent/-/geojson-extent-1.0.0.tgz#cc1db1ca5f28e7a3a2d71dcfc86c9ef3486e1558" + integrity sha512-OWW/Tw7OkKHoogXjQJNILjLd2d4JZzO/elc5Qr08VNwFSIPpSnJgyaEGO2xRPqNuWDLr4RocuqmC0FcQWPgeOA== dependencies: "@mapbox/extent" "0.4.0" - "@mapbox/geojson-coords" "0.0.0" + "@mapbox/geojson-coords" "0.0.1" rw "~0.1.4" traverse "~0.6.6" -"@mapbox/geojson-normalize@0.0.1": +"@mapbox/geojson-normalize@0.0.1", "@mapbox/geojson-normalize@^0.0.1": version "0.0.1" resolved "https://registry.yarnpkg.com/@mapbox/geojson-normalize/-/geojson-normalize-0.0.1.tgz#1da1e6b3a7add3ad29909b30f438f60581b7cd80" integrity sha1-HaHms6et060pkJsw9Dj2BYG3zYA= @@ -2897,17 +2897,6 @@ resolved "https://registry.yarnpkg.com/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz#9aecf642cb00eab1080a57c4f949a65b4a5846d6" integrity sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw== -"@mapbox/geojsonhint@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@mapbox/geojsonhint/-/geojsonhint-3.0.0.tgz#42448232ce4236cb89c1b69c36b0cadeac99e02e" - integrity sha512-zHcyh1rDHYnEBd6NvOWoeHLuvazlDkIjvz9MJx4cKwcKTlfrqgxVnTv1QLnVJnsSU5neJnhQJcgscR/Zl4uYgw== - dependencies: - concat-stream "^1.6.1" - jsonlint-lines "1.7.1" - minimist "1.2.0" - vfile "^4.0.0" - vfile-reporter "^5.1.1" - "@mapbox/hast-util-table-cell-style@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@mapbox/hast-util-table-cell-style/-/hast-util-table-cell-style-0.1.3.tgz#5b7166ae01297d72216932b245e4b2f0b642dca6" @@ -2920,22 +2909,20 @@ resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" integrity sha1-zlblOfg1UrWNENZy6k1vya3HsjQ= -"@mapbox/mapbox-gl-draw@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.2.0.tgz#b6e5278afef65bd5d7d92366034997768e478ad9" - integrity sha512-gMrP2zn8PzDtrs72FMJTPytCumX5vUn9R7IK38qBOVy9UfqbdWr56KYuNA/2X+jKn4FIOpmWf8CWkKpOaQkv7w== +"@mapbox/mapbox-gl-draw@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-draw/-/mapbox-gl-draw-1.3.0.tgz#7a30fb99488cb47a32c25e99c3c62413b04bbaed" + integrity sha512-B+KWK+dAgzLHMNyKVuuMRfjeSlQ77MhNLdfpQQpbp3pkhnrdmydDe3ixto1Ua78hktNut0WTrAaD8gYu4PVcjA== dependencies: - "@mapbox/geojson-area" "^0.2.1" - "@mapbox/geojson-extent" "^0.3.2" - "@mapbox/geojson-normalize" "0.0.1" - "@mapbox/geojsonhint" "3.0.0" - "@mapbox/point-geometry" "0.1.0" - eslint-plugin-import "^2.19.1" + "@mapbox/geojson-area" "^0.2.2" + "@mapbox/geojson-extent" "^1.0.0" + "@mapbox/geojson-normalize" "^0.0.1" + "@mapbox/point-geometry" "^0.1.0" hat "0.0.3" - lodash.isequal "^4.2.0" - xtend "^4.0.1" + lodash.isequal "^4.5.0" + xtend "^4.0.2" -"@mapbox/mapbox-gl-rtl-text@^0.2.3": +"@mapbox/mapbox-gl-rtl-text@0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.3.tgz#a26ecfb3f0061456d93ee8570dd9587d226ea8bd" integrity sha512-RaCYfnxULUUUxNwcUimV9C/o2295ktTyLEUzD/+VWkqXqvaVfFcZ5slytGzb2Sd/Jj4MlbxD0DCZbfa6CzcmMw== @@ -3600,6 +3587,16 @@ resolved "https://registry.yarnpkg.com/@redux-saga/types/-/types-1.1.0.tgz#0e81ce56b4883b4b2a3001ebe1ab298b84237204" integrity sha512-afmTuJrylUU/0OtqzaRkbyYFFNgCF73Bvel/sw90pvGrWIZ+vyoIJqA6eMSoA6+nb443kTmulmBtC9NerXboNg== +"@reduxjs/toolkit@^1.5.1": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.5.1.tgz#05daa2f6eebc70dc18cd98a90421fab7fa565dc5" + integrity sha512-PngZKuwVZsd+mimnmhiOQzoD0FiMjqVks6ituO1//Ft5UEX5Ca9of13NEjo//pU22Jk7z/mdXVsmDfgsig1osA== + dependencies: + immer "^8.0.1" + redux "^4.0.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@samverschueren/stream-to-observable@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f" @@ -5349,10 +5346,10 @@ resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03" integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w== -"@types/mapbox-gl@^1.9.1": - version "1.9.1" - resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-1.9.1.tgz#78b62f8a1ead78bc525a4c1db84bb71fa0fcc579" - integrity sha512-5LS/fljbGjCPfjtOK5+pz8TT0PL4bBXTnN/PDbPtTQMqQdY/KWTWE4jRPuo0fL5wctd543DCptEUTydn+JK+gA== +"@types/mapbox-gl@1.13.1": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-1.13.1.tgz#bd8108f912f32c895117e2970b6d4fbbecbe42a1" + integrity sha512-Yqv1eFAzG2gdecc94higNC8KE+BR6t8QhFgbQGGEpKr3OgSVVtr2qaBNBPaGlIAtCoKDF6JGB2haOhvijYC4Bg== dependencies: "@types/geojson" "*" @@ -5806,6 +5803,13 @@ resolved "https://registry.yarnpkg.com/@types/redux-actions/-/redux-actions-2.6.1.tgz#0940e97fa35ad3004316bddb391d8e01d2efa605" integrity sha512-zKgK+ATp3sswXs6sOYo1tk8xdXTy4CTaeeYrVQlClCjeOpag5vzPo0ASWiiBJ7vsiQRAdb3VkuFLnDoBimF67g== +"@types/redux-logger@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.8.tgz#1fb6d26917bb198792bb1cf57feb31cae1532c5d" + integrity sha512-zM+cxiSw6nZtRbxpVp9SE3x/X77Z7e7YAfHD1NkxJyJbAGSXJGF0E9aqajZfPOa/sTYnuwutmlCldveExuCeLw== + dependencies: + redux "^4.0.0" + "@types/request@^2.48.2": version "2.48.2" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.2.tgz#936374cbe1179d7ed529fc02543deb4597450fed" @@ -6470,11 +6474,6 @@ JSONStream@1.3.5, JSONStream@^1.0.3: jsonparse "^1.2.0" through ">=2.2.7 <3" -"JSV@>= 4.0.x": - version "4.0.2" - resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57" - integrity sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c= - abab@^2.0.0, abab@^2.0.3, abab@^2.0.4: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -6871,11 +6870,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" -ansi-styles@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" - integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg= - ansi-to-html@^0.6.11: version "0.6.13" resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.13.tgz#c72eae8b63e5ca0643aab11bfc6e6f2217425833" @@ -9141,15 +9135,6 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@~4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" - integrity sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8= - dependencies: - ansi-styles "~1.0.0" - has-color "~0.1.0" - strip-ansi "~0.1.0" - chalk@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174" @@ -11316,6 +11301,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ= + deep-eql@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" @@ -12649,7 +12639,7 @@ eslint-formatter-pretty@^4.0.0: string-width "^4.2.0" supports-hyperlinks "^2.0.0" -eslint-import-resolver-node@0.3.2, eslint-import-resolver-node@^0.3.2: +eslint-import-resolver-node@0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== @@ -12681,7 +12671,7 @@ eslint-import-resolver-webpack@0.11.1: resolve "^1.10.0" semver "^5.3.0" -eslint-module-utils@2.5.0, eslint-module-utils@^2.4.1: +eslint-module-utils@2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.5.0.tgz#cdf0b40d623032274ccd2abd7e64c4e524d6e19c" integrity sha512-kCo8pZaNz2dsAW7nCUjuVoI11EBXXpIzfNxmaoLhXoRDOnqXLC4iSGVRdZPhOitfbdEfMEfKOiENaK6wDPZEGw== @@ -12734,24 +12724,6 @@ eslint-plugin-eslint-comments@^3.2.0: escape-string-regexp "^1.0.5" ignore "^5.0.5" -eslint-plugin-import@^2.19.1: - version "2.19.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.19.1.tgz#5654e10b7839d064dd0d46cd1b88ec2133a11448" - integrity sha512-x68131aKoCZlCae7rDXKSAQmbT5DQuManyXo2sK6fJJ0aK5CWAkv6A6HJZGgqC8IhjQxYPgo6/IY4Oz8AFsbBw== - dependencies: - array-includes "^3.0.3" - array.prototype.flat "^1.2.1" - contains-path "^0.1.0" - debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.1" - has "^1.0.3" - minimatch "^3.0.4" - object.values "^1.1.0" - read-pkg-up "^2.0.0" - resolve "^1.12.0" - eslint-plugin-import@^2.22.1: version "2.22.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" @@ -14232,13 +14204,13 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -geojson-flatten@~0.2.1: - version "0.2.4" - resolved "https://registry.yarnpkg.com/geojson-flatten/-/geojson-flatten-0.2.4.tgz#8f3396f31a0f5b747e39c9e6a14088f43ba4ecfb" - integrity sha512-LiX6Jmot8adiIdZ/fthbcKKPOfWjTQchX/ggHnwMZ2e4b0I243N1ANUos0LvnzepTEsj0+D4fIJ5bKhBrWnAHA== +geojson-flatten@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/geojson-flatten/-/geojson-flatten-1.0.4.tgz#cdfef2e9042996fcaa14fe658db6d88c99c20930" + integrity sha512-PpscUXxO6dvvhZxtwuqiI5v+1C/IQYPJRMWoQeaF2oohJgfGYSHKVAe8L+yUqF34PH/hmq9JlwmO+juPw+95/Q== dependencies: - get-stdin "^6.0.0" - minimist "1.2.0" + get-stdin "^7.0.0" + minimist "^1.2.5" geojson-vt@^3.2.1: version "3.2.1" @@ -14311,6 +14283,11 @@ get-stdin@^6.0.0: resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== +get-stdin@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-7.0.0.tgz#8d5de98f15171a125c5e516643c7a6d0ea8a96f6" + integrity sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ== + get-stdin@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-8.0.0.tgz#cbad6a73feb75f6eeb22ba9e01f89aa28aa97a53" @@ -15056,11 +15033,6 @@ has-bigints@^1.0.1: resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== -has-color@~0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" - integrity sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8= - has-flag@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" @@ -17749,14 +17721,6 @@ jsonify@~0.0.0: resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonlint-lines@1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/jsonlint-lines/-/jsonlint-lines-1.7.1.tgz#507de680d3fb8c4be1641cc57d6f679f29f178ff" - integrity sha1-UH3mgNP7jEvhZBzFfW9nnynxeP8= - dependencies: - JSV ">= 4.0.x" - nomnom ">= 1.5.x" - jsonparse@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" @@ -18452,7 +18416,7 @@ lodash.isempty@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4= -lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.2.0, lodash.isequal@^4.5.0: +lodash.isequal@^4.0.0, lodash.isequal@^4.1.1, lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= @@ -18815,7 +18779,7 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -mapbox-gl-draw-rectangle-mode@^1.0.4: +mapbox-gl-draw-rectangle-mode@1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== @@ -19367,7 +19331,7 @@ minimist-options@4.1.0, minimist-options@^4.0.2: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@0.0.8, minimist@1.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: +minimist@0.0.8, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -20214,14 +20178,6 @@ nodemon@^2.0.4: undefsafe "^2.0.3" update-notifier "^4.1.0" -"nomnom@>= 1.5.x": - version "1.8.1" - resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" - integrity sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc= - dependencies: - chalk "~0.4.0" - underscore "~1.6.0" - "nopt@2 || 3", nopt@~3.0.6: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -21264,9 +21220,9 @@ path-key@^3.0.0, path-key@^3.1.0: integrity sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg== path-parse@^1.0.5, path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-platform@~0.11.15: version "0.11.15" @@ -23691,6 +23647,13 @@ redux-devtools-extension@^2.13.8: resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg== +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8= + dependencies: + deep-diff "^0.3.5" + redux-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7" @@ -24097,7 +24060,7 @@ repeat-element@^1.1.2: resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" integrity sha1-7wiaF40Ug7quTZPrmLT55OEdmQo= -repeat-string@^1.0.0, repeat-string@^1.5.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: +repeat-string@^1.0.0, repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= @@ -25879,7 +25842,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -26027,11 +25990,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" - integrity sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE= - strip-bom-string@1.X: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" @@ -27486,7 +27444,7 @@ underscore.string@~3.3.5: sprintf-js "^1.0.3" util-deprecate "^1.0.2" -underscore@^1.13.1, underscore@^1.8.3, underscore@~1.6.0: +underscore@^1.13.1, underscore@^1.8.3: version "1.13.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1" integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g== @@ -28618,28 +28576,6 @@ vfile-message@^1.0.0: dependencies: unist-util-stringify-position "^1.1.1" -vfile-reporter@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/vfile-reporter/-/vfile-reporter-5.1.1.tgz#419688c7e9dcaf65ba81bfdb0ad443e9e0248e09" - integrity sha512-A/cfKvfVmeEmAKx1yyOWggCjC/k184Vkl5pVJAw5CEdppHd5FHBVcdyJ1JBSqIdJjJqyhZY4ZD3JycHr/uwmlA== - dependencies: - repeat-string "^1.5.0" - string-width "^2.0.0" - supports-color "^5.4.0" - unist-util-stringify-position "^1.0.0" - vfile-sort "^2.1.2" - vfile-statistics "^1.1.0" - -vfile-sort@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/vfile-sort/-/vfile-sort-2.2.0.tgz#383a8727ec4c5daf37c05683684a5eb686366d39" - integrity sha512-RgxLXVWrJBWb2GuP8FsSkqK7HmbjXjnI8qx3nD6NTWhsWaelaKvJuxfh1F1d1lkCPD7imo4zzi8cf6IOMgaTnQ== - -vfile-statistics@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vfile-statistics/-/vfile-statistics-1.1.2.tgz#c50132627e4669a3afa07c64ff1e7aa7695e8151" - integrity sha512-16wAC9eEGXdsD35LX9m/iXCRIZyX5LIrDgDtAF92rbATSqsBRbC4n05e0Rj5vt3XRpcKu0UJeWnTxWsSyvNZ+w== - vfile@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a"